From f436cf3edc2f8ff2e0df27d9172b68143149e470 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Fri, 23 Sep 2022 09:18:16 -0700 Subject: [PATCH] Initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jake Coffman Co-authored-by: David Rodríguez Co-authored-by: Jurre Stender --- .github/SECURITY.md | 4 + .github/dependabot.yml | 6 + .github/workflows/ci.yml | 42 +++ .github/workflows/codeql-analysis.yml | 72 +++++ .github/workflows/release.yml | 31 +++ .github/workflows/smoke.yml | 86 ++++++ .gitignore | 3 + .golangci.yml | 61 +++++ Brewfile | 9 + Brewfile.lock.json | 104 +++++++ CODEOWNERS | 1 + LICENSE | 21 ++ README.md | 236 ++++++++++++++++ cmd/dependabot/dependabot.go | 9 + cmd/dependabot/internal/cmd/root.go | 48 ++++ cmd/dependabot/internal/cmd/test.go | 87 ++++++ cmd/dependabot/internal/cmd/update.go | 197 ++++++++++++++ cmd/dependabot/internal/cmd/version.go | 44 +++ docs/debugging-with-the-cli.md | 72 +++++ docs/design.md | 96 +++++++ docs/e2e-tests.md | 45 ++++ docs/img/diff.png | Bin 0 -> 118446 bytes docs/release.md | 20 ++ go.mod | 36 +++ go.sum | 120 +++++++++ internal/infra/cadetails.go | 79 ++++++ internal/infra/cadetails_test.go | 19 ++ internal/infra/config.go | 44 +++ internal/infra/network.go | 57 ++++ internal/infra/password.go | 14 + internal/infra/proxy.go | 145 ++++++++++ internal/infra/proxy_test.go | 16 ++ internal/infra/run.go | 233 ++++++++++++++++ internal/infra/run_test.go | 169 ++++++++++++ internal/infra/tempdir.go | 30 +++ internal/infra/tempdir_test.go | 18 ++ internal/infra/tty.go | 99 +++++++ internal/infra/updater.go | 323 ++++++++++++++++++++++ internal/model/job.go | 86 ++++++ internal/model/job_test.go | 360 +++++++++++++++++++++++++ internal/model/scenario.go | 25 ++ internal/model/update.go | 53 ++++ internal/server/api.go | 247 +++++++++++++++++ internal/server/input.go | 45 ++++ internal/server/input_test.go | 37 +++ script/download-cache.sh | 16 ++ script/run-all.sh | 9 + testdata/go/allow-go.yaml | 112 ++++++++ testdata/go/close-pr.yaml | 51 ++++ testdata/go/security-go.yaml | 117 ++++++++ testdata/go/update-pr.yaml | 108 ++++++++ testdata/private-bundler.json | 22 ++ testdata/private-npm.yaml | 12 + 53 files changed, 3996 insertions(+) create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/smoke.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Brewfile create mode 100644 Brewfile.lock.json create mode 100644 CODEOWNERS create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/dependabot/dependabot.go create mode 100644 cmd/dependabot/internal/cmd/root.go create mode 100644 cmd/dependabot/internal/cmd/test.go create mode 100644 cmd/dependabot/internal/cmd/update.go create mode 100644 cmd/dependabot/internal/cmd/version.go create mode 100644 docs/debugging-with-the-cli.md create mode 100644 docs/design.md create mode 100644 docs/e2e-tests.md create mode 100644 docs/img/diff.png create mode 100644 docs/release.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/infra/cadetails.go create mode 100644 internal/infra/cadetails_test.go create mode 100644 internal/infra/config.go create mode 100644 internal/infra/network.go create mode 100644 internal/infra/password.go create mode 100644 internal/infra/proxy.go create mode 100644 internal/infra/proxy_test.go create mode 100644 internal/infra/run.go create mode 100644 internal/infra/run_test.go create mode 100644 internal/infra/tempdir.go create mode 100644 internal/infra/tempdir_test.go create mode 100644 internal/infra/tty.go create mode 100644 internal/infra/updater.go create mode 100644 internal/model/job.go create mode 100644 internal/model/job_test.go create mode 100644 internal/model/scenario.go create mode 100644 internal/model/update.go create mode 100644 internal/server/api.go create mode 100644 internal/server/input.go create mode 100644 internal/server/input_test.go create mode 100755 script/download-cache.sh create mode 100755 script/run-all.sh create mode 100644 testdata/go/allow-go.yaml create mode 100644 testdata/go/close-pr.yaml create mode 100644 testdata/go/security-go.yaml create mode 100644 testdata/go/update-pr.yaml create mode 100644 testdata/private-bundler.json create mode 100644 testdata/private-npm.yaml diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..0154e77 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,4 @@ +If you discover a security issue in this repository, +please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github). + +Thanks for helping make GitHub safe for everyone. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5b6b53a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8f21ee4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI Build + Unit Test + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.49 + + - name: Run shellcheck + run: shellcheck **/*.sh diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..828a92d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '22 17 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + 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. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # 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@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8a1da2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release binary builder + +on: + release: + types: [created] + +jobs: + releases-matrix: + name: Release Go Binary + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux, windows, darwin] + goarch: ["386", amd64, arm64] + exclude: + - goarch: "386" + goos: darwin + - goarch: arm64 + goos: windows + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1.29 + with: + goversion: 1.19 + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + binary_name: dependabot + project_path: cmd/dependabot + ldflags: >- + -X github.com/dependabot/cli/cmd/dependabot/internal/cmd.version=${{ github.event.release.tag_name }} diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..575cd71 --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,86 @@ +# Runs all ecosystems cached and concurrently. +name: Smoke + +on: + workflow_dispatch: + pull_request: + branches: ["main"] + +env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + smoke: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + - actions + - bundler + - cargo + - composer + - docker + - elm + - go + - gradle + - hex + - maven + - npm + - nuget + - pip + - pip-compile + - pipenv + - poetry + - pub + - submodules + - terraform + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + # Download the Proxy cache. The job is ideally 100% cached so no real calls are made. + - name: Download artifacts + run: script/download-cache.sh ${{ matrix.suite }} + + - name: ${{ matrix.suite }} + env: + LOCAL_GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -o pipefail + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/smoke-${{ matrix.suite }}.yaml + curl $(gh api $URL --jq .download_url) -o smoke.yaml + go run cmd/dependabot/dependabot.go test -f=smoke.yaml -o=result.yaml --timeout 20m --cache=cache 2>&1 | tee -a log.txt + + - name: Diff + if: always() + continue-on-error: true + run: diff --ignore-space-change smoke.yaml result.yaml && echo "Contents are identical" || exit 0 + + - name: Create summary + run: tail -n100 log.txt | grep -P '\d+/\d+ calls cached \(\d+%\)' >> $GITHUB_STEP_SUMMARY + + # No upload at the end: + # - If a test is uncachable in some regard, the cache would grow unbound. + # - We might want to consider erroring if the cache is changed. + + # Allows us to add a check requirement on allsmoke which covers all in the matrix above + allsmoke: + if: ${{ always() }} + runs-on: ubuntu-latest + name: Smoke result + needs: smoke + steps: + - name: Echo needs + run: echo "${{ toJSON(needs) }}" # for debugging + - name: Check success + run: | + if [ "${{ needs.smoke.result }}" = "success" ]; then + exit 0 + else + exit 1 + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7613027 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +tmp +testdata/caches +cache diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..41ecda8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,61 @@ +run: + tests: true + skip-dirs: + - test-updater + +linters: + enable: + - depguard + - errcheck + - exportloopref + - gocritic + - gocyclo + - gofmt + - goimports + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unused + disable: + - gochecknoglobals # we allow global variables in packages + - gochecknoinits # we allow inits in packages + - goconst # we allow repeated values to go un-const'd + - lll # we allow any line length + - structcheck # structcheck is disabled because of go1.18 + - unparam # we allow function calls to name unused parameters + +linters-settings: + errcheck: + check-type-assertions: true + goconst: + min-len: 2 + min-occurrences: 3 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - hugeParam + - octalLiteral + - singleCaseSwitch + govet: + check-shadowing: true + nolintlint: + require-explanation: true + require-specific: true + +issues: + exclude-rules: + - path: internal/infra/proxy.go + text: "G306: Expect WriteFile permissions to be 0600 or less" diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..c4e2102 --- /dev/null +++ b/Brewfile @@ -0,0 +1,9 @@ +# Usage: +# $ brew bundle + +tap 'homebrew/core' + +brew 'go' + +tap 'golangci/tap' +brew 'golangci-lint' diff --git a/Brewfile.lock.json b/Brewfile.lock.json new file mode 100644 index 0000000..b18aac6 --- /dev/null +++ b/Brewfile.lock.json @@ -0,0 +1,104 @@ +{ + "entries": { + "tap": { + "homebrew/core": { + "revision": "8c764825c6668815b12df4058bccd6c15495e900" + }, + "golangci/tap": { + "revision": "7e8d1b4b1c31c15d9918a31bcc1a110b2454d29a" + } + }, + "brew": { + "go": { + "version": "1.19", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_monterey": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:053b3a83d7a9f47ec3b435e3681a2e3ea29ac7d267fe1f97e144b2512c736eb9", + "sha256": "053b3a83d7a9f47ec3b435e3681a2e3ea29ac7d267fe1f97e144b2512c736eb9" + }, + "arm64_big_sur": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:4bc6f293bc5c42caab60cd2e2821e12f3a4f0a76c2afca347efdb2e8715f972a", + "sha256": "4bc6f293bc5c42caab60cd2e2821e12f3a4f0a76c2afca347efdb2e8715f972a" + }, + "monterey": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:0c072d504cbf5ac584d85f46882afc51db91d341e97c9a2c83bc74b980b8409b", + "sha256": "0c072d504cbf5ac584d85f46882afc51db91d341e97c9a2c83bc74b980b8409b" + }, + "big_sur": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:97671397aef87943379ddd6a8f4c56eca31da2d63db57d25fea9e1ce01fbd7be", + "sha256": "97671397aef87943379ddd6a8f4c56eca31da2d63db57d25fea9e1ce01fbd7be" + }, + "catalina": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:0fe6f9f864e16a828c88fef98ea89ed03eb7e5c1bc5c83151d0e952fa276b719", + "sha256": "0fe6f9f864e16a828c88fef98ea89ed03eb7e5c1bc5c83151d0e952fa276b719" + }, + "x86_64_linux": { + "cellar": "/home/linuxbrew/.linuxbrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:5814764a0a39b6ada34106e5775a437c667d9f907041e2a2c0059c780a8a3a1a", + "sha256": "5814764a0a39b6ada34106e5775a437c667d9f907041e2a2c0059c780a8a3a1a" + } + } + } + }, + "golangci-lint": { + "version": "1.49.0", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golangci-lint/blobs/sha256:790eba5d113226ccd2a5fad4ad6d5bf5aafc7a9dda6a32cd08bc032e74b98a9b", + "sha256": "790eba5d113226ccd2a5fad4ad6d5bf5aafc7a9dda6a32cd08bc032e74b98a9b" + }, + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golangci-lint/blobs/sha256:c0239bd2993c5a46f42de9f8bd30bc3700279370e9660e61d4d908a9623cb236", + "sha256": "c0239bd2993c5a46f42de9f8bd30bc3700279370e9660e61d4d908a9623cb236" + }, + "monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golangci-lint/blobs/sha256:013ea67d8c8a6bd7276bd689ed44fb7e0072f2b2e93be6029c2582803dd5fe25", + "sha256": "013ea67d8c8a6bd7276bd689ed44fb7e0072f2b2e93be6029c2582803dd5fe25" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golangci-lint/blobs/sha256:f4c69256964d8d727743870f55c7740361566fb32ed45461c3270b0436cebe53", + "sha256": "f4c69256964d8d727743870f55c7740361566fb32ed45461c3270b0436cebe53" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golangci-lint/blobs/sha256:72eb2f241f8a43f9dbffbf282108345a8e1c5f41cb78edda625f9d9e0b40f84d", + "sha256": "72eb2f241f8a43f9dbffbf282108345a8e1c5f41cb78edda625f9d9e0b40f84d" + }, + "x86_64_linux": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golangci-lint/blobs/sha256:3656e3077a2e9fd6746c1e22c736c5e07b532d2c235829b05d12dac6062fdfe8", + "sha256": "3656e3077a2e9fd6746c1e22c736c5e07b532d2c235829b05d12dac6062fdfe8" + } + } + } + } + } + }, + "system": { + "macos": { + "monterey": { + "HOMEBREW_VERSION": "3.5.10-49-gb2ddb34", + "HOMEBREW_PREFIX": "/usr/local", + "Homebrew/homebrew-core": "8c764825c6668815b12df4058bccd6c15495e900", + "CLT": "12.0.0.32.29", + "Xcode": "dunno", + "macOS": "12.5.1" + } + } + } +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..248dab4 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @dependabot/maintainers diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..00e9069 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c496f73 --- /dev/null +++ b/README.md @@ -0,0 +1,236 @@ +# Dependabot CLI + +A CLI that pulls in the [updater] and [proxy] containers and runs them. + +## Installation + +You can download a pre-built binary for your system from the [Releases] page. + +If you have the [`gh` CLI][gh] installed, +you can install the latest release of `dependabot` using the following command +([gist source](https://gist.github.com/mattt/e09e1ecd76d5573e0517a7622009f06f)): + +```console +$ gh gist view --raw e09e1ecd76d5573e0517a7622009f06f | bash +``` + +## Requirements + +* Docker + +For development: + +* Go 1.18+ + +## Usage + +```console +$ dependabot update go_modules rsc/quote --dry-run +``` + +
+Output + +``` +time="2022-07-25T11:35:05Z" level=info msg="proxy starting" commit=8bc7edd876c7b566c70dcf22daa1c039912767f9 +time="2022-07-25T11:35:05Z" level=warning msg="initializing metrics client" error="No address passed and autodetection from environment failed" +2022/07/25 11:35:05 Listening (:1080) +I, [2022-07-25T11:35:06.684817 #9] INFO -- sentry: ** [Raven] Raven 3.1.2 configured not to capture errors: DSN not set +INFO Starting job processing +2022/07/25 11:35:28 [002] GET https://api.github.com:443/repos/rsc/quote +2022/07/25 11:35:28 [002] * authenticating github api request +2022/07/25 11:35:28 [002] 200 https://api.github.com:443/repos/rsc/quote +2022/07/25 11:35:28 [004] GET https://api.github.com:443/repos/rsc/quote/git/refs/heads/master +2022/07/25 11:35:28 [004] * authenticating github api request +2022/07/25 11:35:28 [004] 200 https://api.github.com:443/repos/rsc/quote/git/refs/heads/master +2022/07/25 11:35:29 [006] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:29 [006] * authenticating git server request (host: github.com) +2022/07/25 11:35:29 [006] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:29 [008] POST https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:29 [008] * authenticating git server request (host: github.com) +2022/07/25 11:35:29 [008] 200 https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:29 [010] POST https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:29 [010] * authenticating git server request (host: github.com) +2022/07/25 11:35:29 [010] 200 https://github.com:443/rsc/quote/git-upload-pack +INFO Finished job processing +I, [2022-07-25T11:35:30.005259 #30] INFO -- sentry: ** [Raven] Raven 3.1.2 configured not to capture errors: DSN not set +INFO Starting job processing +INFO Starting update job for rsc/quote +2022/07/25 11:35:51 [011] POST http://host.docker.internal:8080/update_jobs/cli/update_dependency_list +2022/07/25 11:35:51 [011] 200 http://host.docker.internal:8080/update_jobs/cli/update_dependency_list +INFO Checking if rsc.io/quote/v3 3.0.0 needs updating +2022/07/25 11:35:51 [013] GET https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:35:51 [013] 200 https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:35:51 [015] GET https://rsc.io:443/quote?go-get=1 +2022/07/25 11:35:51 [015] 200 https://rsc.io:443/quote?go-get=1 +2022/07/25 11:35:51 [017] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:51 [017] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [017] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [019] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [019] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [019] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [021] POST https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:52 [021] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [021] 200 https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:52 [023] POST https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:52 [023] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [023] 200 https://github.com:443/rsc/quote/git-upload-pack +INFO Latest version is 3.1.0 +INFO Requirements to unlock own +INFO Requirements update strategy +INFO Updating rsc.io/quote/v3 from 3.0.0 to 3.1.0 +2022/07/25 11:35:52 [026] GET https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:35:52 [027] GET https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:35:52 [026] 200 https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:35:52 [029] GET https://rsc.io:443/quote?go-get=1 +2022/07/25 11:35:52 [027] 200 https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:35:52 [029] 200 https://rsc.io:443/quote?go-get=1 +2022/07/25 11:35:52 [032] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [032] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [033] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [033] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [032] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [033] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [035] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [035] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [037] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [037] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [035] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [037] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:52 [039] POST https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:52 [039] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [041] POST https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:52 [041] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [039] 200 https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:52 [043] POST https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:52 [043] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [041] 200 https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:52 [045] POST https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:52 [045] * authenticating git server request (host: github.com) +2022/07/25 11:35:52 [043] 200 https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:52 [045] 200 https://github.com:443/rsc/quote/git-upload-pack +2022/07/25 11:35:53 [047] GET https://golang.org:443/x/text?go-get=1 +2022/07/25 11:35:53 [047] 200 https://golang.org:443/x/text?go-get=1 +2022/07/25 11:35:53 [049] GET https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:35:53 [049] 200 https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:35:53 [051] GET https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:35:53 [051] 200 https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:35:53 [053] POST https://go.googlesource.com:443/text/git-upload-pack +2022/07/25 11:35:53 [053] 200 https://go.googlesource.com:443/text/git-upload-pack +2022/07/25 11:35:57 [055] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:57 [055] * authenticating git server request (host: github.com) +2022/07/25 11:35:57 [055] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:57 [057] POST https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:57 [057] * authenticating git server request (host: github.com) +2022/07/25 11:35:57 [057] 200 https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:57 [059] POST https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:57 [059] * authenticating git server request (host: github.com) +2022/07/25 11:35:57 [059] 200 https://github.com:443/rsc/sampler/git-upload-pack +2022/07/25 11:35:57 [062] GET https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:35:57 [063] GET https://golang.org:443/x/text?go-get=1 +2022/07/25 11:35:57 [062] 200 https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:35:57 [065] GET https://rsc.io:443/quote?go-get=1 +2022/07/25 11:35:57 [065] 200 https://rsc.io:443/quote?go-get=1 +2022/07/25 11:35:57 [063] 200 https://golang.org:443/x/text?go-get=1 +2022/07/25 11:35:58 [068] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:58 [068] * authenticating git server request (host: github.com) +2022/07/25 11:35:58 [069] GET https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:35:58 [069] 200 https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:35:58 [068] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:35:58 [071] GET https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:35:58 [071] 200 https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:35:58 [073] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:35:58 [073] * authenticating git server request (host: github.com) +2022/07/25 11:35:58 [073] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +INFO Submitting rsc.io/quote/v3 pull request for creation +2022/07/25 11:36:18 [074] POST http://host.docker.internal:8080/update_jobs/cli/create_pull_request +2022/07/25 11:36:18 [074] 200 http://host.docker.internal:8080/update_jobs/cli/create_pull_request +INFO Checking if rsc.io/sampler 1.3.0 needs updating +2022/07/25 11:36:18 [076] GET https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:36:18 [076] 200 https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:36:18 [078] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [078] * authenticating git server request (host: github.com) +2022/07/25 11:36:18 [078] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +INFO Latest version is 1.99.99 +INFO Requirements to unlock own +INFO Requirements update strategy +INFO Updating rsc.io/sampler from 1.3.0 to 1.99.99 +2022/07/25 11:36:18 [080] GET https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:36:18 [080] 200 https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:36:18 [082] GET https://golang.org:443/x/text?go-get=1 +2022/07/25 11:36:18 [084] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [084] * authenticating git server request (host: github.com) +2022/07/25 11:36:18 [082] 200 https://golang.org:443/x/text?go-get=1 +2022/07/25 11:36:18 [086] GET https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [084] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [086] 200 https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [088] GET https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:36:18 [088] 200 https://rsc.io:443/quote/v3?go-get=1 +2022/07/25 11:36:18 [090] GET https://rsc.io:443/quote?go-get=1 +2022/07/25 11:36:18 [090] 200 https://rsc.io:443/quote?go-get=1 +2022/07/25 11:36:18 [092] GET https://golang.org:443/x/text?go-get=1 +2022/07/25 11:36:18 [094] GET https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [094] * authenticating git server request (host: github.com) +2022/07/25 11:36:18 [092] 200 https://golang.org:443/x/text?go-get=1 +2022/07/25 11:36:18 [096] GET https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:36:18 [094] 200 https://github.com:443/rsc/quote/info/refs?service=git-upload-pack +2022/07/25 11:36:19 [098] GET https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:36:19 [096] 200 https://go.googlesource.com:443/text/info/refs?service=git-upload-pack +2022/07/25 11:36:19 [098] 200 https://rsc.io:443/sampler?go-get=1 +2022/07/25 11:36:19 [100] GET https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +2022/07/25 11:36:19 [100] * authenticating git server request (host: github.com) +2022/07/25 11:36:19 [100] 200 https://github.com:443/rsc/sampler/info/refs?service=git-upload-pack +INFO Submitting rsc.io/sampler pull request for creation +2022/07/25 11:36:39 [101] POST http://host.docker.internal:8080/update_jobs/cli/create_pull_request +2022/07/25 11:36:39 [101] 200 http://host.docker.internal:8080/update_jobs/cli/create_pull_request +2022/07/25 11:36:59 [102] PATCH http://host.docker.internal:8080/update_jobs/cli/mark_as_processed +2022/07/25 11:36:59 [102] 200 http://host.docker.internal:8080/update_jobs/cli/mark_as_processed +INFO Finished job processing +INFO Results: ++----------------------------------------------------+ +| Changes to Dependabot Pull Requests | ++---------+------------------------------------------+ +| created | rsc.io/quote/v3 ( from 3.0.0 to 3.1.0 ) | +| created | rsc.io/sampler ( from 1.3.0 to 1.99.99 ) | ++---------+------------------------------------------+ +``` + +
+ +## Troubleshooting + +### Docker daemon not running + +``` +failed to pull ghcr.io/github/dependabot-update-job-proxy/dependabot-update-job-proxy:latest: +Error response from daemon: dial unix docker.raw.sock: connect: no such file or directory +``` + +The CLI requires Docker to be running on your machine. +Follow the instructions on [Docker's website](https://docs.docker.com/get-started/) +to get the latest version of Docker installed and running. + +You can verify that Docker is running locally with the following command: + +```console +$ docker --version +``` + +### Network internet is ambiguous + +``` +failed to start container: Error response from daemon: network internet is ambiguous (2 matches found on name) +``` + +This error can occur when the CLI exits before getting to clean up +(e.g. terminating with ^C). +Run the following command to remove all unused networks: + +```console +$ docker network prune +``` + +[updater]: https://github.com/dependabot/dependabot-core/pkgs/container/dependabot-updater +[proxy]: https://github.com/github/dependabot-update-job-proxy/pkgs/container/dependabot-update-job-proxy%2Fdependabot-update-job-proxy +[gh]: https://github.com/cli/cli +[Releases]: https://github.com/dependabot/cli/releases diff --git a/cmd/dependabot/dependabot.go b/cmd/dependabot/dependabot.go new file mode 100644 index 0000000..0437936 --- /dev/null +++ b/cmd/dependabot/dependabot.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/dependabot/cli/cmd/dependabot/internal/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/cmd/dependabot/internal/cmd/root.go b/cmd/dependabot/internal/cmd/root.go new file mode 100644 index 0000000..ff6a1eb --- /dev/null +++ b/cmd/dependabot/internal/cmd/root.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "os" + "time" + + "github.com/dependabot/cli/internal/infra" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +var ( + file string + cache string + debugging bool + output string + pullImages bool + volumes []string + timeout time.Duration + tempDir string +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "dependabot [flags]", + Short: "Dependabot end-to-end runner", + Long: `Run Dependabot jobs from the command line.`, + Example: heredoc.Doc(` + $ dependabot update go_modules rsc/quote --dry-run + $ dependabot test -f input.yml + `), + Version: Version(), +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().StringVar(&infra.UpdaterImageName, "updater-image", infra.UpdaterImageName, "container image to use for the updater") + rootCmd.PersistentFlags().StringVar(&infra.ProxyImageName, "proxy-image", infra.ProxyImageName, "container image to use for the proxy") + + rootCmd.PersistentFlags().StringVar(&tempDir, "temp-dir", "tmp", "path to the temporary directory for the job") +} diff --git a/cmd/dependabot/internal/cmd/test.go b/cmd/dependabot/internal/cmd/test.go new file mode 100644 index 0000000..3ca9b49 --- /dev/null +++ b/cmd/dependabot/internal/cmd/test.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "github.com/dependabot/cli/internal/infra" + "github.com/dependabot/cli/internal/model" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var ( + jobs int +) + +var testCmd = &cobra.Command{ + Use: "test [-f file]", + Short: "Test scenarios", + RunE: func(cmd *cobra.Command, args []string) error { + if jobs < 1 { + return fmt.Errorf("workers must be greater than or equal to 1") + } + + if file == "" { + return fmt.Errorf("requires a scenario file") + } + + scenario, err := readScenarioFile(file) + if err != nil { + return err + } + + processInput(&scenario.Input) + + if err := infra.Run(infra.RunParams{ + CacheDir: cache, + Creds: scenario.Input.Credentials, + Debug: debugging, + Expected: scenario.Output, + Job: &scenario.Input.Job, + Output: output, + PullImages: pullImages, + TempDir: tempDir, + Timeout: timeout, + Volumes: volumes, + }); err != nil { + log.Fatal(err) + } + + return nil + }, +} + +func readScenarioFile(file string) (*model.Scenario, error) { + var scenario model.Scenario + + data, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("failed to open scenario file: %w", err) + } + if err = json.Unmarshal(data, &scenario); err != nil { + if err = yaml.Unmarshal(data, &scenario); err != nil { + return nil, fmt.Errorf("failed to decode scenario file: %w", err) + } + } + + return &scenario, nil +} + +func init() { + rootCmd.AddCommand(testCmd) + + testCmd.Flags().StringVarP(&file, "file", "f", "", "path to scenario file") + testCmd.Flags().IntVarP(&jobs, "jobs", "j", 1, "Number of jobs to run simultaneously") + testCmd.MarkFlagsMutuallyExclusive("jobs", "file") + + testCmd.Flags().StringVarP(&output, "output", "o", "", "write scenario to file") + testCmd.Flags().StringVar(&cache, "cache", "", "cache import/export directory") + testCmd.Flags().BoolVar(&pullImages, "pull", true, "pull the image if it isn't present") + testCmd.Flags().BoolVar(&debugging, "debug", false, "run an interactive shell inside the updater") + testCmd.Flags().StringArrayVarP(&volumes, "volume", "v", nil, "mount volumes in Docker") + testCmd.Flags().DurationVarP(&timeout, "timeout", "t", 0, "max time to run an update") +} diff --git a/cmd/dependabot/internal/cmd/update.go b/cmd/dependabot/internal/cmd/update.go new file mode 100644 index 0000000..191111f --- /dev/null +++ b/cmd/dependabot/internal/cmd/update.go @@ -0,0 +1,197 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + + "github.com/MakeNowJust/heredoc" + "github.com/dependabot/cli/internal/infra" + "github.com/dependabot/cli/internal/model" + "github.com/dependabot/cli/internal/server" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var ( + packageManager string + provider string + repo string + directory string + + dryRun bool + inputServerPort int +) + +var updateCmd = &cobra.Command{ + Use: "update [flags]", + Short: "Perform update job", + Example: heredoc.Doc(` + $ dependabot update go_modules rsc/quote --dry-run + `), + RunE: func(cmd *cobra.Command, args []string) error { + var outFile *os.File + if output != "" { + var err error + outFile, err = os.Create(output) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + } + + input := &model.Input{} + + if file != "" { + var err error + input, err = readInputFile(file) + if err != nil { + return err + } + } + + if len(cmd.Flags().Args()) > 0 { + packageManager = cmd.Flags().Args()[0] + if packageManager == "" { + return errors.New("requires a package manager argument") + } + + repo = cmd.Flags().Args()[1] + if repo == "" { + return errors.New("requires a repo argument") + } + + input.Job = model.Job{ + PackageManager: packageManager, + AllowedUpdates: []model.Allowed{{ + UpdateType: "all", + }}, + Dependencies: nil, + ExistingPullRequests: [][]model.ExistingPR{}, + IgnoreConditions: []model.Condition{}, + LockfileOnly: false, + RequirementsUpdateStrategy: nil, + SecurityAdvisories: []model.Advisory{}, + SecurityUpdatesOnly: false, + Source: model.Source{ + Provider: provider, + Repo: repo, + Directory: directory, + Branch: nil, + Hostname: nil, + APIEndpoint: nil, + }, + UpdateSubdependencies: false, + UpdatingAPullRequest: false, + } + } + + if inputServerPort != 0 { + input = server.Input(inputServerPort) + } + + if doesStdinHaveData() { + in := &bytes.Buffer{} + _, err := io.Copy(in, os.Stdin) + if err != nil { + return err + } + data := in.Bytes() + if err = json.Unmarshal(data, &input); err != nil { + if err = yaml.Unmarshal(data, &input); err != nil { + return fmt.Errorf("failed to decode input file: %w", err) + } + } + } + + processInput(input) + + if err := infra.Run(infra.RunParams{ + CacheDir: cache, + Creds: input.Credentials, + Debug: debugging, + Expected: nil, // update subcommand doesn't use expectations + Job: &input.Job, + Output: output, + PullImages: pullImages, + TempDir: tempDir, + Timeout: timeout, + Volumes: volumes, + }); err != nil { + log.Fatalf("failed to run updater: %v", err) + } + + return nil + }, +} + +func readInputFile(file string) (*model.Input, error) { + var input model.Input + + data, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("failed to open input file: %w", err) + } + if err = json.Unmarshal(data, &input); err != nil { + if err = yaml.Unmarshal(data, &input); err != nil { + return nil, fmt.Errorf("failed to decode input file: %w", err) + } + } + + return &input, nil +} + +func processInput(input *model.Input) { + job := &input.Job + // a few of the fields need to be initialized instead of null, + // it would be nice if the updater didn't care + if job.ExistingPullRequests == nil { + job.ExistingPullRequests = [][]model.ExistingPR{} + } + if job.IgnoreConditions == nil { + job.IgnoreConditions = []model.Condition{} + } + if job.SecurityAdvisories == nil { + job.SecurityAdvisories = []model.Advisory{} + } + + // Process environment variables in the scenario file + for _, cred := range input.Credentials { + for key, value := range cred { + cred[key] = os.ExpandEnv(value) + } + } +} + +func doesStdinHaveData() bool { + file := os.Stdin + fi, err := file.Stat() + if err != nil { + fmt.Println("file.Stat()", err) + } + return fi.Size() > 0 +} + +func init() { + rootCmd.AddCommand(updateCmd) + + updateCmd.Flags().StringVarP(&file, "file", "f", "", "path to scenario file") + + updateCmd.Flags().StringVarP(&provider, "provider", "p", "github", "provider of the repository") + updateCmd.Flags().StringVarP(&directory, "directory", "d", "/", "directory to update") + + updateCmd.Flags().BoolVar(&dryRun, "dry-run", true, "perform update as a dry run") + _ = updateCmd.MarkFlagRequired("dry-run") + + updateCmd.Flags().StringVarP(&output, "output", "o", "", "write scenario to file") + updateCmd.Flags().StringVar(&cache, "cache", "", "cache import/export directory") + updateCmd.Flags().BoolVar(&pullImages, "pull", true, "pull the image if it isn't present") + updateCmd.Flags().BoolVar(&debugging, "debug", false, "run an interactive shell inside the updater") + updateCmd.Flags().StringArrayVarP(&volumes, "volume", "v", nil, "mount volumes in Docker") + updateCmd.Flags().DurationVarP(&timeout, "timeout", "t", 0, "max time to run an update") + updateCmd.Flags().IntVar(&inputServerPort, "input-port", 0, "port to use for securely passing input to the updater") +} diff --git a/cmd/dependabot/internal/cmd/version.go b/cmd/dependabot/internal/cmd/version.go new file mode 100644 index 0000000..eb2777f --- /dev/null +++ b/cmd/dependabot/internal/cmd/version.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "fmt" + "regexp" + "runtime/debug" +) + +// ldflags inserts the version here on release +var version string + +var timestampRegex = regexp.MustCompile("[^a-zA-Z0-9]+") + +func Version() string { + if version == "" { + version = "0.0.0-dev" + commit := "" + timestamp := "" + modified := false + + info, _ := debug.ReadBuildInfo() + for _, entry := range info.Settings { + if entry.Key == "vcs.revision" && len(entry.Value) >= 7 { + commit = entry.Value[:7] // short ref + } + + if entry.Key == "vcs.modified" { + modified = entry.Value == "true" + } + + if entry.Key == "vcs.time" { + timestamp = timestampRegex.ReplaceAllString(entry.Value, "") + } + } + + if modified && timestamp != "" { + return fmt.Sprintf("%s+%s", version, timestamp) + } else if commit != "" { + return fmt.Sprintf("%s+%s", version, commit) + } + } + + return version +} diff --git a/docs/debugging-with-the-cli.md b/docs/debugging-with-the-cli.md new file mode 100644 index 0000000..58dc21a --- /dev/null +++ b/docs/debugging-with-the-cli.md @@ -0,0 +1,72 @@ +# Debugging with the CLI + +Updater is now merged into core, so we want the CLI to become the preferred way to debug issues in core. This is because the CLI runs the proxy and updater just like how Dependabot runs in production. + +To get started, run the dependabot script in core's script directory. This is mostly equivalent to running `bin/dry-run.rb` inside a docker-dev-shell, but without needing to boot a shell in the container first. + +If you want to debug like you do with docker-dev-shell, you can pass the `--debug` flag to `script/dependabot`. This will mount core's ecosystems for editing while debugging just like the docker-dev-shell does. + +For general debugging of an ecosystem, use the update subcommand, and pass the ecosystem and NWO (name with org) like so: + +```console +$ script/dependabot update go_modules rsc/quote --dry-run --debug +``` + +The CLI will use a Personal Access Token (PAT) in the environment variable `LOCAL_GITHUB_ACCESS_TOKEN` and pass that to the Proxy before starting it. + +It will then generate a `job.json` file with `package_manager` of "go_modules" and a `source` of "rsc/quote". + +Unlike the dry-run script and dev-shell, we no longer run the shell while dry-running several repositories. It is recommended you exit the debugging session and start a new one when switching the debugging target. Although you could edit the job.json, the CLI starts very quickly and that's probably not necessary. You may even want to restart after changes to the native helpers. + +Once you are in the debugging session, run `bin/run fetch_files` which is the first step the updater takes when it runs. Once complete, run `bin/run update_files` to perform the update. + +You might notice the proxy output is not logged in the terminal. To see that you'll have to tail the logs in another terminal or using Docker Desktop. Alternatively you can [fix this issue](https://github.com/dependabot/cli/issues/87), and it will work. + +## More complex scenarios + +For debugging of more complex situations, you'll want to use a YAML file for input. + +View the `testdata` directory for examples of inputs. The input section is all you'll need. So for instance you write a YAML file like this: + +```yaml +job: + package_manager: npm_and_yarn + allowed_updates: + - update-type: all + security_advisories: + - dependency-name: express + affected-versions: + - <5.0.0 + patched-versions: [] + unaffected-versions: [] + security_updates_only: true + source: + provider: github + repo: dependabot/e2e-tests + directory: / + commit: 66115359e6f6cc3af6a661c5d5ae803720b98cb8 +credentials: + - type: npm_registry + registry: https://npm.pkg.github.com + token: $GPR_TOKEN +``` + +And then run your debugging session like so: + +```console +$ script/dependabot update -f test.yaml --dry-run --debug +``` + +The CLI will process any `$VARIABLES` in YAML files and replace them with environment variables (for example, `$LOCAL_GITHUB_ACCESS_TOKEN`). + +Or if you just want to run a complete update and view the results, run it without the `--debug` flag + +```console +$ script/dependabot update -f test.yaml -o output.yaml --dry-run +``` + +The `output.yaml` file produced shows all the calls the updater will have made during the course of the update. It also happens to be input to an end-to-end test: + +```console +$ script/dependabot test -f output.yaml +``` diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..64c7b4d --- /dev/null +++ b/docs/design.md @@ -0,0 +1,96 @@ +# Design + +The CLI is designed to start minimal Dependabot infrastructure on your machine in order to run an update. + +At a high level, the CLI: +- Pulls an Updater image and a Proxy image +- Creates networks so the Updater can make calls only through the Proxy +- Writes the Proxy input file +- Starts the Proxy +- Writes the job file, which is the input to the Updater +- Starts the Updater image +- Records calls for creating pull requests from the Updater +- Waits for the Updater image to finish +- Cleans everything up + +This setup is identical to how [dependabot-action](https://github.com/github/dependabot-action) works, and pretty similar to how our production setup works. So by running with the CLI, we can test essentially what is in production. + +## CLI Goals + +- Create one way to run Dependabot + - Makes it easier to test End-to-End + - Reduce the amount of code the team supports +- Customers can build custom logic downstream from Dependabot CLI by creating adapters +- External users who integrate with other systems can create adapters too + +The CLI also opens a lot of doors around extensibility and maintainability of ecosystems. + +## Sequence Diagrams + +All Updater calls go through the Proxy, I've elided those for brevity. + +### E2E tests + +Generating tests with a --dry-run: + +```mermaid +sequenceDiagram + CLI->>Proxy: Starts the Proxy + CLI->>Updater: Starts the Updater + Updater->>GitHub: Fetch manifests + loop + Updater->>Registry: Get version info, etc + Updater->>CLI: Create/Update PR, etc + end + CLI->>YAML file: CLI was given -o so it outputs the calls made +``` + +Asserting expected behavior, fully cached in the Proxy: + +```mermaid +sequenceDiagram + CLI->>Proxy: Starts the Proxy + CLI->>Updater: Starts the Updater + Updater->>Proxy: Fetch manifests (cached) + loop + Updater->>Proxy: Get version info, etc (cached) + Updater->>CLI: Create/Update PR, etc + end + CLI->>stderr: CLI outputs pass/fail info, exit code +``` + +### On Desktop + +Future phase + +```mermaid +sequenceDiagram + CLI->>Proxy: Starts the Proxy + CLI->>Updater: Starts the Updater + Updater->>GitHub: Fetch manifests + loop + Updater->>Registry: Get version info, etc + Updater->>CLI: Create/Update PR + CLI->>GH Adapter: CLI forwards API calls to an adapter (TBD) + GH Adapter->>GitHub: Adds PAT header to Create/Update PR + end +``` + + +### In GHES + +Future phase + +```mermaid +sequenceDiagram + Workflow->>CLI: Invoke with Job + CLI->>Proxy: Starts the Proxy + CLI->>Updater: Starts the Updater + Updater->>GitHub: Fetch manifests + loop + Updater->>Registry: Get version info, etc + Updater->>CLI: Create/Update PR + CLI->>GHES Adapter: CLI forwards API calls to an adapter (TBD) + GHES Adapter->>Dependabot API: Adds auth headers + end +``` diff --git a/docs/e2e-tests.md b/docs/e2e-tests.md new file mode 100644 index 0000000..9f06c34 --- /dev/null +++ b/docs/e2e-tests.md @@ -0,0 +1,45 @@ +# End-to-End tests + +The Dependabot CLI includes features to produce and run end-to-end (e2e) tests. + +## Producing a test + +- Create a repository with the manifest and files you want to test on GitHub +- Generate the test file by doing a dry-run + - For example: `dependabot update bundler dependabot/e2e-tests -o=testdata/smoke-bundler.yaml --dry-run` +- Test the cache effectiveness by running the `test` command twice with the cache option. Make sure you start with a clean cache initially. + - `dependabot test -f=testdata/smoke-bundler.yaml --cache=tmp/cache` + - At the end of the test you'll see an output like this: `time="2022-08-11T00:34:41Z" level=info msg="71/316 calls cached (22%)"` + - If the cache coverage isn't 100% the test will most likely fail when a new dependency is published +- If you are producing a new smoke test: + - Add entries in the workflows: + - cache-all.yml + - cache-one.yml + - smoke.yml + - Put up the PR + - Run the cache-one workflow on the new ecosystem to fill the cache + - Run the smoke workflow to verify tests are passing, and cache coverage is good + - You may also need to update the smoke workflow in updater or core + +## Debugging a failure + +When an e2e test fails in a workflow, it produces a diff between the test expectations and the actual output: + +![diff output](img/diff.png) + +Use this information to decide if the test failure is just the ecosystem somehow evading caching, or if it's a real failure. + +To test locally: +- Optionally download the exiting cache with, for example `cd tmp/cache && gh run download --repo dependabot/cli --name cache-bundler` +- Run the test: `dependabot test -f=testdata/smoke-bundler.yaml -cache=tmp/cache` + - Overwrite the existing test for easy diffing with git: `dependabot test -f=testdata/smoke-bundler.yaml -o=testdata/smoke-bundler.yaml -cache=tmp/cache` + +## Working with the Actions cache + +The caching in Actions is handled by build artifacts. Each cache has a unique name, like `cache-bundler`. + +To create a new cache, run the `cache-one` workflow. + +To create all new caches, run `cache-all`. It's rare to do this, and takes a long time, so recommended you stick to `cache-one`. + +To download them for local testing, run `gh run download --repo dependabot/cli --name cache-bundler`. diff --git a/docs/img/diff.png b/docs/img/diff.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb3ed4db33b5adbaa97d6eae3e936b700e35700 GIT binary patch literal 118446 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 ucF*CwD)*=b_o>)$|IH!J@+C#?m_lBl0D;Ia^wR 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 literal 0 HcmV?d00001 diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..726e8b2 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,20 @@ +# Releasing the CLI + +- Go to the [releases] page and note the last release. Decide if this will be major, minor, or patch. + - During our pre-release we are bumping patch + - Choose minor for new features and fixes + - Choose patch if you are back-porting a fix to an older version + - Choose major for breaking changes to command line flags or any other ways the CLI works +- Click [Draft a new release] +- Click "Choose a tag" +- Type in the new version preceded with a `v` +- Click "Create new tag" which is just under where you typed +- Optionally create a Title and add release notes +- Click "Publish release" + +The `release.yml` workflow will kick off and add binaries to the release after a couple of minutes. + +Feel free to automate this in the future! + +[releases]: https://github.com/dependabot/cli/releases +[Draft a new release]: https://github.com/dependabot/cli/releases/new diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..26d7467 --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/dependabot/cli + +go 1.19 + +require ( + github.com/MakeNowJust/heredoc v1.0.0 + github.com/docker/cli v20.10.18+incompatible + github.com/docker/docker v20.10.18+incompatible + github.com/moby/sys/signal v0.7.0 + github.com/sergi/go-diff v1.2.0 + github.com/spf13/cobra v1.5.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.0 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + gotest.tools/v3 v3.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..730015b --- /dev/null +++ b/go.sum @@ -0,0 +1,120 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/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/docker/cli v20.10.18+incompatible h1:f/GQLsVpo10VvToRay2IraVA1wHz9KktZyjev3SIVDU= +github.com/docker/cli v20.10.18+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc= +github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/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-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/internal/infra/cadetails.go b/internal/infra/cadetails.go new file mode 100644 index 0000000..c0367c1 --- /dev/null +++ b/internal/infra/cadetails.go @@ -0,0 +1,79 @@ +package infra + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" +) + +const ( + keySize = 2048 + keyExpiryYears = 2 +) + +var certSubject = pkix.Name{ + CommonName: "Dependabot Internal CA", + OrganizationalUnit: []string{"Dependabot"}, + Organization: []string{"GitHub Inc."}, + Locality: []string{"San Francisco"}, + Province: []string{"California"}, + Country: []string{"US"}, +} + +// GenerateCertificateAuthority generates a new proxy keypair CA +func GenerateCertificateAuthority() (CertificateAuthority, error) { + key, pemKey, err := generateKey() + if err != nil { + return CertificateAuthority{}, err + } + + pemCert, err := generateCert(key) + if err != nil { + return CertificateAuthority{}, err + } + + return CertificateAuthority{ + Cert: pemCert, + Key: pemKey, + }, nil +} + +func generateKey() (*rsa.PrivateKey, string, error) { + key, err := rsa.GenerateKey(rand.Reader, keySize) + if err != nil { + return nil, "", err + } + kb := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + return key, string(pem.EncodeToMemory(kb)), nil +} + +func generateCert(key *rsa.PrivateKey) (string, error) { + notBefore := time.Now() + notAfter := notBefore.AddDate(keyExpiryYears, 0, 0) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: certSubject, + NotBefore: notBefore, + NotAfter: notAfter, + SignatureAlgorithm: x509.SHA256WithRSA, + BasicConstraintsValid: true, + IsCA: true, + } + cert, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + return "", err + } + cb := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + } + return string(pem.EncodeToMemory(cb)), nil +} diff --git a/internal/infra/cadetails_test.go b/internal/infra/cadetails_test.go new file mode 100644 index 0000000..a6155ca --- /dev/null +++ b/internal/infra/cadetails_test.go @@ -0,0 +1,19 @@ +package infra + +import ( + "strings" + "testing" +) + +func TestGenerateCaDetails(t *testing.T) { + ca, err := GenerateCertificateAuthority() + if err != nil { + t.Fatal(err.Error()) + } + if !strings.Contains(ca.Cert, "BEGIN CERTIFICATE") { + t.Errorf("Expected certificate to contain BEGIN CERTIFICATE, got %s", ca.Cert) + } + if !strings.Contains(ca.Key, "BEGIN RSA PRIVATE KEY") { + t.Errorf("Expected certificate to contain BEGIN RSA PRIVATE KEY, got %s", ca.Key) + } +} diff --git a/internal/infra/config.go b/internal/infra/config.go new file mode 100644 index 0000000..cd7cf28 --- /dev/null +++ b/internal/infra/config.go @@ -0,0 +1,44 @@ +package infra + +import ( + "encoding/json" + "fmt" + "os" +) + +// ConfigFilePath is the path to proxy config file. +const ConfigFilePath = "/config.json" + +// Config is the structure of the proxy's config file +type Config struct { + Credentials []map[string]string `json:"all_credentials"` + CA CertificateAuthority `json:"ca"` + ProxyAuth BasicAuthCredentials `json:"proxy_auth"` +} + +// CertificateAuthority includes the MITM CA certificate and private key +type CertificateAuthority struct { + Cert string `json:"cert"` + Key string `json:"key"` +} + +// BasicAuthCredentials represents credentials required for HTTP basic auth +type BasicAuthCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// StoreProxyConfig saves the config to a temporary file, returning the path +func StoreProxyConfig(tmpPath string, config *Config) (string, error) { + tmp, err := os.CreateTemp(TempDir(tmpPath), "config.json") + if err != nil { + return "", fmt.Errorf("creating proxy config: %w", err) + } + defer tmp.Close() + + if err := json.NewEncoder(tmp).Encode(config); err != nil { + _ = os.RemoveAll(tmp.Name()) + return "", fmt.Errorf("encoding proxy config: %w", err) + } + return tmp.Name(), nil +} diff --git a/internal/infra/network.go b/internal/infra/network.go new file mode 100644 index 0000000..7ecc06a --- /dev/null +++ b/internal/infra/network.go @@ -0,0 +1,57 @@ +package infra + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/namesgenerator" +) + +type Networks struct { + NoInternet types.NetworkCreateResponse + Internet types.NetworkCreateResponse + cli *client.Client + noInternetName string + internetName string +} + +func NewNetworks(ctx context.Context, cli *client.Client) (*Networks, error) { + const bridge = "bridge" + + noInternetName := namesgenerator.GetRandomName(1) + noInternet, err := cli.NetworkCreate(ctx, noInternetName, types.NetworkCreate{ + Internal: true, + Driver: bridge, + }) + if err != nil { + return nil, fmt.Errorf("failed to create no-internet network: %w", err) + } + + internetName := namesgenerator.GetRandomName(1) + internet, err := cli.NetworkCreate(ctx, internetName, types.NetworkCreate{ + Driver: bridge, + }) + if err != nil { + return nil, fmt.Errorf("failed to create internet network: %w", err) + } + + return &Networks{ + cli: cli, + NoInternet: noInternet, + Internet: internet, + noInternetName: noInternetName, + internetName: internetName, + }, nil +} + +func (n *Networks) Close() error { + if err := n.cli.NetworkRemove(context.Background(), n.NoInternet.ID); err != nil { + return err + } + if err := n.cli.NetworkRemove(context.Background(), n.Internet.ID); err != nil { + return err + } + return nil +} diff --git a/internal/infra/password.go b/internal/infra/password.go new file mode 100644 index 0000000..7d7f65f --- /dev/null +++ b/internal/infra/password.go @@ -0,0 +1,14 @@ +package infra + +import ( + "crypto/rand" + "encoding/hex" +) + +const passwordLength = 24 + +func generatePassword() string { + b := make([]byte, passwordLength) + _, _ = rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/internal/infra/proxy.go b/internal/infra/proxy.go new file mode 100644 index 0000000..e6245e6 --- /dev/null +++ b/internal/infra/proxy.go @@ -0,0 +1,145 @@ +package infra + +import ( + "context" + "fmt" + "math/rand" + "os" + "path/filepath" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/namesgenerator" + "github.com/docker/docker/pkg/stdcopy" +) + +func init() { + // needed for namesgenerator.GetRandomName + rand.Seed(time.Now().UnixNano()) +} + +// ProxyImageName is the docker image used by the proxy +var ProxyImageName = "ghcr.io/github/dependabot-update-job-proxy/dependabot-update-job-proxy:latest" + +type Proxy struct { + cli *client.Client + containerID string + CertPath string + proxyConfigPath string + containerName string + url string +} + +func NewProxy(ctx context.Context, cli *client.Client, params *RunParams, nets ...types.NetworkCreateResponse) (*Proxy, error) { + // Generate secrets: + ca, err := GenerateCertificateAuthority() + if err != nil { + return nil, fmt.Errorf("failed to generate cert: %w", err) + } + certPath := filepath.Join(TempDir(params.TempDir), "cert.crt") + os.Remove(certPath) + + err = os.WriteFile(certPath, []byte(ca.Cert), 0777) + if err != nil { + return nil, fmt.Errorf("failed to write cert: %w", err) + } + + // Generate and write configuration to disk: + username := "cli-user" + password := generatePassword() + proxyConfig := &Config{ + Credentials: params.Creds, + ProxyAuth: BasicAuthCredentials{ + Username: username, + Password: password, + }, + CA: ca, + } + proxyConfigPath, err := StoreProxyConfig(params.TempDir, proxyConfig) + if err != nil { + return nil, fmt.Errorf("failed to store proxy config: %w", err) + } + + hostCfg := &container.HostConfig{ + AutoRemove: true, + Mounts: []mount.Mount{{ + Type: mount.TypeBind, + Source: proxyConfigPath, + Target: ConfigFilePath, + ReadOnly: true, + }}, + ExtraHosts: []string{ + "host.docker.internal:host-gateway", + }, + } + if params.CacheDir != "" { + _ = os.MkdirAll(params.CacheDir, 0744) + cacheDir, _ := filepath.Abs(params.CacheDir) + hostCfg.Mounts = append(hostCfg.Mounts, mount.Mount{ + Type: mount.TypeBind, + Source: cacheDir, + Target: "/cache", + }) + } + config := &container.Config{ + Image: ProxyImageName, + Env: []string{ + "JOB_ID=" + jobID, + "PROXY_CACHE=true", + }, + } + hostName := namesgenerator.GetRandomName(1) + proxyContainer, err := cli.ContainerCreate(ctx, config, hostCfg, nil, nil, hostName) + if err != nil { + return nil, fmt.Errorf("failed to create proxy container: %w", err) + } + for _, n := range nets { + if err := cli.NetworkConnect(ctx, n.ID, proxyContainer.ID, &network.EndpointSettings{}); err != nil { + return nil, fmt.Errorf("failed to connect to network: %w", err) + } + } + + if err := cli.ContainerStart(ctx, proxyContainer.ID, types.ContainerStartOptions{}); err != nil { + return nil, fmt.Errorf("failed to start container: %w", err) + } + + return &Proxy{ + cli: cli, + containerID: proxyContainer.ID, + containerName: hostName, + url: fmt.Sprintf("http://%s:%s@%s:1080", username, password, hostName), + CertPath: certPath, + proxyConfigPath: proxyConfigPath, + }, nil +} + +func (p *Proxy) TailLogs(ctx context.Context, cli *client.Client) { + out, err := cli.ContainerLogs(ctx, p.containerID, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + return + } + + _, _ = stdcopy.StdCopy(os.Stdout, os.Stderr, out) +} + +func (p *Proxy) Close() error { + defer os.Remove(p.CertPath) + defer os.Remove(p.proxyConfigPath) + + timeout := 5 * time.Second + _ = p.cli.ContainerStop(context.Background(), p.containerID, &timeout) + + err := p.cli.ContainerRemove(context.Background(), p.containerID, types.ContainerRemoveOptions{Force: true}) + if err != nil { + return fmt.Errorf("failed to remove proxy container: %w", err) + } + return nil +} diff --git a/internal/infra/proxy_test.go b/internal/infra/proxy_test.go new file mode 100644 index 0000000..ebe6235 --- /dev/null +++ b/internal/infra/proxy_test.go @@ -0,0 +1,16 @@ +package infra + +import ( + "testing" + + "github.com/docker/docker/pkg/namesgenerator" +) + +func TestSeed(t *testing.T) { + // ensure we're still seeding + a := namesgenerator.GetRandomName(1) + b := namesgenerator.GetRandomName(1) + if a == b { + t.Error("Not seeding math/rand") + } +} diff --git a/internal/infra/run.go b/internal/infra/run.go new file mode 100644 index 0000000..4a9ff3b --- /dev/null +++ b/internal/infra/run.go @@ -0,0 +1,233 @@ +package infra + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/dependabot/cli/internal/server" + + "github.com/dependabot/cli/internal/model" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "gopkg.in/yaml.v3" +) + +type RunParams struct { + // job definition passed to the updater + Job *model.Job + // expectations asserted at the end of a test + Expected []model.Output + // credentials passed to the proxy + Creds []map[string]string + // local directory used for caching + CacheDir string + // write output to a file + Output string + // attempt to pull images if they aren't local? + PullImages bool + // run an interactive shell? + Debug bool + // Volumes are used to mount directories in Docker + Volumes []string + // Timeout specifies an optional maximum duration the CLI will run an update. + // If Timeout is <= 0 it will never time out. + Timeout time.Duration + // TempDir is the path to use as the temporary directory. + TempDir string +} + +func Run(params RunParams) error { + var ctx context.Context + var cancel func() + if params.Timeout > 0 { + ctx, cancel = context.WithTimeout(context.Background(), params.Timeout) + } else { + ctx, cancel = context.WithCancel(context.Background()) + } + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signals + cancel() + }() + + api := server.NewAPI(params.Expected) + defer api.Stop() + + token := os.Getenv("LOCAL_GITHUB_ACCESS_TOKEN") + if token != "" { + params.Creds = append(params.Creds, map[string]string{ + "type": "git_source", + "host": "github.com", + "username": "x-access-token", + "password": token, + }) + } + + var outFile *os.File + if params.Output != "" { + var err error + // Open a file for writing but don't truncate it yet since an error will delete the test. + // This is done before the test so if the dir isn't writable it doesn't waste time. + outFile, err = os.OpenFile(params.Output, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + } + + if err := runContainers(ctx, params, api); err != nil { + return err + } + + api.Complete() + if outFile != nil { + if err := outFile.Truncate(0); err != nil { + return fmt.Errorf("failed to truncate output file: %w", err) + } + if params.Job.Source.Commit == nil { + // store the SHA we worked with for reproducible tests + params.Job.Source.Commit = api.Actual.Input.Job.Source.Commit + } + api.Actual.Input.Job = *params.Job + + // ignore conditions help make tests reproducible + // so they are generated if there aren't any yet + if len(api.Actual.Input.Job.IgnoreConditions) == 0 && api.Actual.Input.Job.PackageManager != "submodules" { + for _, out := range api.Actual.Output { + if out.Type == "create_pull_request" { + createPR, ok := out.Expect.Data.(model.CreatePullRequest) + if !ok { + return fmt.Errorf("failed to decode CreatePullRequest object") + } + + for _, dep := range createPR.Dependencies { + ignore := model.Condition{ + DependencyName: dep.Name, + VersionRequirement: fmt.Sprintf(">%v", *dep.Version), + Source: params.Output, + } + api.Actual.Input.Job.IgnoreConditions = append(api.Actual.Input.Job.IgnoreConditions, ignore) + } + } + } + } + if err := yaml.NewEncoder(outFile).Encode(api.Actual); err != nil { + return fmt.Errorf("failed to write output: %v", err) + } + } + if len(api.Errors) > 0 { + log.Println("The following errors occurred:") + + for _, e := range api.Errors { + log.Println(e) + } + + return fmt.Errorf("update failed expectations") + } + + return nil +} + +func runContainers(ctx context.Context, params RunParams, api *server.API) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + + if params.PullImages { + err = pullImage(ctx, cli, ProxyImageName) + if err != nil { + return err + } + + err = pullImage(ctx, cli, UpdaterImageName) + if err != nil { + return err + } + } + + networks, err := NewNetworks(ctx, cli) + if err != nil { + return fmt.Errorf("failed to create networks: %w", err) + } + defer networks.Close() + + prox, err := NewProxy(ctx, cli, ¶ms, networks.NoInternet, networks.Internet) + if err != nil { + return err + } + defer prox.Close() + + // proxy logs interfere with debugging output + if !params.Debug { + go prox.TailLogs(ctx, cli) + } + + updater, err := NewUpdater(ctx, cli, networks, ¶ms, prox) + if err != nil { + return err + } + defer updater.Close() + + if err := updater.InstallCertificates(ctx); err != nil { + return err + } + + if params.Debug { + if err := updater.RunShell(ctx, prox.url, api.Port()); err != nil { + return err + } + } else { + if err := updater.RunUpdate(ctx, prox.url, api.Port()); err != nil { + return err + } + } + + return nil +} + +func pullImage(ctx context.Context, cli *client.Client, image string) error { + var inspect types.ImageInspect + + // check if image exists locally + inspect, _, err := cli.ImageInspectWithRaw(ctx, image) + + // pull image if necessary + if err != nil { + var privilegeFunc types.RequestPrivilegeFunc + token := os.Getenv("LOCAL_GITHUB_ACCESS_TOKEN") + if token != "" { + auth := base64.StdEncoding.EncodeToString([]byte("x:" + token)) + privilegeFunc = func() (string, error) { + return "Basic " + auth, nil + } + } + + log.Printf("pulling image: %s\n", image) + out, err := cli.ImagePull(ctx, image, types.ImagePullOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err != nil { + return fmt.Errorf("failed to pull %v: %w", image, err) + } + _, _ = io.Copy(io.Discard, out) + out.Close() + + inspect, _, err = cli.ImageInspectWithRaw(ctx, image) + if err != nil { + return fmt.Errorf("failed to inspect %v: %w", image, err) + } + } + + log.Printf("using image %v at %s\n", image, inspect.ID) + + return nil +} diff --git a/internal/infra/run_test.go b/internal/infra/run_test.go new file mode 100644 index 0000000..0d2496d --- /dev/null +++ b/internal/infra/run_test.go @@ -0,0 +1,169 @@ +package infra + +import ( + "archive/tar" + "bytes" + "context" + "io" + "testing" + + "github.com/dependabot/cli/internal/model" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" +) + +func TestRun(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + + var buildContext bytes.Buffer + tw := tar.NewWriter(&buildContext) + addFileToArchive(tw, "/Dockerfile", 0644, dockerFile) + addFileToArchive(tw, "/test_main.go", 0644, testMain) + tw.Close() + + UpdaterImageName = "test-updater" + resp, err := cli.ImageBuild(ctx, &buildContext, types.ImageBuildOptions{Tags: []string{UpdaterImageName}}) + if err != nil { + t.Fatal(err) + } + + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + + defer func() { + _, _ = cli.ImageRemove(ctx, UpdaterImageName, types.ImageRemoveOptions{}) + }() + + err = Run(RunParams{ + PullImages: true, + Job: &model.Job{ + PackageManager: "ecosystem", + Source: model.Source{ + Repo: "org/name", + }, + }, + TempDir: "/tmp", + }) + if err != nil { + t.Error(err) + } +} + +func addFileToArchive(tw *tar.Writer, path string, mode int64, content string) { + header := &tar.Header{ + Name: path, + Size: int64(len(content)), + Mode: mode, + } + + err := tw.WriteHeader(header) + if err != nil { + panic(err) + } + + _, err = io.Copy(tw, bytes.NewReader([]byte(content))) + if err != nil { + panic(err) + } +} + +const dockerFile = ` +FROM golang:1.19 + +# needed to run update-ca-certificates +RUN apt-get update && apt-get install -y ca-certificates +# cli will try to start as dependabot +RUN useradd -ms /bin/bash dependabot + +# need to be the user for permissions to work +USER dependabot +WORKDIR /home/dependabot +COPY *.go . +# cli runs bin/run (twice) to do an update, so put exe there +RUN go mod init cli_test && go mod tidy && go build -o bin/run +` + +const testMain = `package main + +import ( + "bytes" + "context" + "encoding/xml" + "log" + "net" + "net/http" + "os" + "os/exec" + "strings" + "sync" + "time" +) + +func main() { + if os.Args[1] == "update_files" { + return + } + var wg sync.WaitGroup + + // print the line that fails + log.SetFlags(log.Lshortfile) + + go connectivityCheck(&wg) + checkIfRoot() + proxyCheck() + + wg.Wait() +} + +func checkIfRoot() { + buf := &bytes.Buffer{} + cmd := exec.Command("id", "-u") + cmd.Stdout = buf + if err := cmd.Run(); err != nil { + log.Fatalln(err) + } + userID := strings.TrimSpace(buf.String()) + if userID == "0" { + log.Fatalln("User is root") + } +} + +func connectivityCheck(wg *sync.WaitGroup) { + wg.Add(1) + + var d net.Dialer + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := d.DialContext(ctx, "tcp", "1.1.1.1:22") + if err != nil && err.Error() != "dial tcp 1.1.1.1:22: i/o timeout" { + log.Fatalln(err) + } + if err == nil { + log.Fatalln("a connection shouldn't be possible") + } + + wg.Done() +} + +func proxyCheck() { + res, err := http.Get("https://example.com") + if err != nil { + log.Fatalln(err) + } + defer res.Body.Close() + var v any + if err = xml.NewDecoder(res.Body).Decode(&v); err != nil { + log.Fatalln(err) + } +} +` diff --git a/internal/infra/tempdir.go b/internal/infra/tempdir.go new file mode 100644 index 0000000..542bee2 --- /dev/null +++ b/internal/infra/tempdir.go @@ -0,0 +1,30 @@ +package infra + +import ( + "os" + "path" +) + +// TempDir centralizes where the temporary directory is created. +func TempDir(tmpPath string) string { + if path.IsAbs(tmpPath) { + mkdirAll(tmpPath) + return tmpPath + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + tmpPath = path.Join(wd, tmpPath) + mkdirAll(tmpPath) + return tmpPath +} + +func mkdirAll(tmpPath string) { + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + panic(err) + } +} diff --git a/internal/infra/tempdir_test.go b/internal/infra/tempdir_test.go new file mode 100644 index 0000000..6e4f71d --- /dev/null +++ b/internal/infra/tempdir_test.go @@ -0,0 +1,18 @@ +package infra + +import ( + "strings" + "testing" +) + +func TestTempDir(t *testing.T) { + tmp := TempDir("tmp") + if !strings.HasPrefix(tmp, "/") { + t.Errorf("TempDir() = %v, want absolute path", tmp) + } + + tmp = TempDir("/tmp/testing") + if tmp != "/tmp/testing" { + t.Errorf("TempDir() = %v, want /tmp/testing", tmp) + } +} diff --git a/internal/infra/tty.go b/internal/infra/tty.go new file mode 100644 index 0000000..3a2f070 --- /dev/null +++ b/internal/infra/tty.go @@ -0,0 +1,99 @@ +package infra + +import ( + "context" + "log" + "os" + gosignal "os/signal" + "runtime" + "time" + + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/moby/sys/signal" +) + +// NOTE: This file was copied from https://github.com/docker/cli/command/container/tty.go +// and adapted to not pull in the whole docker CLI object. + +// resizeTtyTo resizes tty to specific height and width +func resizeTtyTo(ctx context.Context, c *client.Client, id string, height, width uint, isExec bool) error { + if height == 0 && width == 0 { + return nil + } + + options := types.ResizeOptions{ + Height: height, + Width: width, + } + + var err error + if isExec { + err = c.ContainerExecResize(ctx, id, options) + } else { + err = c.ContainerResize(ctx, id, options) + } + + if err != nil { + log.Printf("Error resize: %s\r", err) + } + return err +} + +// resizeTty is to resize the tty with cli out's tty size +func resizeTty(ctx context.Context, out *streams.Out, cli *client.Client, id string, isExec bool) error { + height, width := out.GetTtySize() + return resizeTtyTo(ctx, cli, id, height, width, isExec) +} + +// initTtySize is to init the tty's size to the same as the window, if there is an error, it will retry 10 times. +func initTtySize(ctx context.Context, out *streams.Out, cli *client.Client, id string, isExec bool, resizeTtyFunc func(context.Context, *streams.Out, *client.Client, string, bool) error) { + rttyFunc := resizeTtyFunc + if rttyFunc == nil { + rttyFunc = resizeTty + } + if err := rttyFunc(ctx, out, cli, id, isExec); err != nil { + go func() { + var err error + for retry := 0; retry < 10; retry++ { + time.Sleep(time.Duration(retry+1) * 10 * time.Millisecond) + if err = rttyFunc(ctx, out, cli, id, isExec); err == nil { + break + } + } + if err != nil { + log.Println("failed to resize tty, using default size") + } + }() + } +} + +// MonitorTtySize updates the container tty size when the terminal tty changes size +func MonitorTtySize(ctx context.Context, out *streams.Out, cli *client.Client, id string, isExec bool) error { + initTtySize(ctx, out, cli, id, isExec, resizeTty) + if runtime.GOOS == "windows" { + go func() { + prevH, prevW := out.GetTtySize() + for { + time.Sleep(time.Millisecond * 250) + h, w := out.GetTtySize() + + if prevW != w || prevH != h { + _ = resizeTty(ctx, out, cli, id, isExec) + } + prevH = h + prevW = w + } + }() + } else { + sigchan := make(chan os.Signal, 1) + gosignal.Notify(sigchan, signal.SIGWINCH) + go func() { + for range sigchan { + _ = resizeTty(ctx, out, cli, id, isExec) + } + }() + } + return nil +} diff --git a/internal/infra/updater.go b/internal/infra/updater.go new file mode 100644 index 0000000..26ac03d --- /dev/null +++ b/internal/infra/updater.go @@ -0,0 +1,323 @@ +package infra + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/dependabot/cli/internal/model" + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" +) + +const jobID = "cli" +const dependabot = "dependabot" + +// UpdaterImageName is the docker image used by the updater +var UpdaterImageName = "ghcr.io/dependabot/dependabot-updater:latest" + +const ( + fetcherOutputFile = "output.json" + fetcherRepoDir = "repo" + guestInputDir = "/home/dependabot/dependabot-updater/job.json" + guestOutputDir = "/home/dependabot/dependabot-updater/output" + guestRepoDir = "/home/dependabot/dependabot-updater/repo" +) + +type Updater struct { + cli *client.Client + containerID string + outputDir string + RepoDir string + inputPath string +} + +const ( + certsPath = "/etc/ssl/certs" + dbotCert = "/usr/local/share/ca-certificates/dbot-ca.crt" +) + +// NewUpdater starts the update container interactively running /bin/sh, so it does not stop. +func NewUpdater(ctx context.Context, cli *client.Client, net *Networks, params *RunParams, prox *Proxy) (*Updater, error) { + f := FileFetcherJobFile{Job: *params.Job} + inputPath, err := WriteContainerInput(params.TempDir, f) + if err != nil { + return nil, fmt.Errorf("failed to write fetcher input: %w", err) + } + + containerCfg := &container.Config{ + User: dependabot, + Image: UpdaterImageName, + Cmd: []string{"/bin/sh"}, + Tty: true, // prevent container from stopping + } + outputDir, err := SetupOutputDir(params.TempDir) + if err != nil { + return nil, fmt.Errorf("failed to setup fetcher output dir: %w", err) + } + + repoDir := filepath.Join(outputDir, fetcherRepoDir) + hostCfg := &container.HostConfig{ + Mounts: []mount.Mount{{ + Type: mount.TypeBind, + Source: inputPath, + Target: guestInputDir, + }, { + Type: mount.TypeBind, + Source: outputDir, + Target: guestOutputDir, + }, { + Type: mount.TypeBind, + Source: prox.CertPath, + Target: dbotCert, + ReadOnly: true, + }}, + } + for _, v := range params.Volumes { + local, remote, _ := strings.Cut(v, ":") + hostCfg.Mounts = append(hostCfg.Mounts, mount.Mount{ + Type: mount.TypeBind, + Source: local, + Target: remote, + }) + } + netCfg := &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + net.noInternetName: { + NetworkID: net.NoInternet.ID, + }, + }, + } + + updaterContainer, err := cli.ContainerCreate(ctx, containerCfg, hostCfg, netCfg, nil, "") + if err != nil { + return nil, fmt.Errorf("failed to create updater container: %w", err) + } + + if err := cli.ContainerStart(ctx, updaterContainer.ID, types.ContainerStartOptions{}); err != nil { + return nil, fmt.Errorf("failed to start updater container: %w", err) + } + + updater := &Updater{ + cli: cli, + containerID: updaterContainer.ID, + outputDir: outputDir, + RepoDir: repoDir, + inputPath: inputPath, + } + + return updater, nil +} + +// InstallCertificates runs update-ca-certificates as root, blocks until complete. +func (u *Updater) InstallCertificates(ctx context.Context) error { + execCreate, err := u.cli.ContainerExecCreate(ctx, u.containerID, types.ExecConfig{ + AttachStdout: true, + AttachStderr: true, + User: "root", + Cmd: []string{"update-ca-certificates"}, + }) + if err != nil { + return fmt.Errorf("failed to create exec: %w", err) + } + + execResp, err := u.cli.ContainerExecAttach(ctx, execCreate.ID, types.ExecStartCheck{}) + if err != nil { + return fmt.Errorf("failed to start exec: %w", err) + } + defer execResp.Close() + + // block until certs are installed or ctl-c + ch := make(chan struct{}) + go func() { + _, _ = stdcopy.StdCopy(os.Stdout, os.Stderr, execResp.Reader) + ch <- struct{}{} + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-ch: + } + + return nil +} + +func userEnv(proxyURL string, apiPort int) []string { + return []string{ + fmt.Sprintf("http_proxy=%s", proxyURL), + fmt.Sprintf("HTTP_PROXY=%s", proxyURL), + fmt.Sprintf("https_proxy=%s", proxyURL), + fmt.Sprintf("HTTPS_PROXY=%s", proxyURL), + fmt.Sprintf("DEPENDABOT_JOB_ID=%v", jobID), + fmt.Sprintf("DEPENDABOT_JOB_TOKEN=%v", ""), + fmt.Sprintf("DEPENDABOT_JOB_PATH=%v", guestInputDir), + fmt.Sprintf("DEPENDABOT_OUTPUT_PATH=%v", filepath.Join(guestOutputDir, fetcherOutputFile)), + fmt.Sprintf("DEPENDABOT_REPO_CONTENTS_PATH=%v", guestRepoDir), + fmt.Sprintf("DEPENDABOT_API_URL=http://host.docker.internal:%v", apiPort), + fmt.Sprintf("SSL_CERT_FILE=%v/ca-certificates.crt", certsPath), + "UPDATER_ONE_CONTAINER=true", + "UPDATER_DETERMINISTIC=true", + } +} + +// RunShell executes an interactive shell, blocks until complete. +func (u *Updater) RunShell(ctx context.Context, proxyURL string, apiPort int) error { + execCreate, err := u.cli.ContainerExecCreate(ctx, u.containerID, types.ExecConfig{ + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + User: dependabot, + Env: userEnv(proxyURL, apiPort), + Cmd: []string{"/bin/bash"}, + }) + if err != nil { + return fmt.Errorf("failed to create exec: %w", err) + } + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + execResp, err := u.cli.ContainerExecAttach(ctx, execCreate.ID, types.ExecStartCheck{}) + if err != nil { + return fmt.Errorf("failed to start exec: %w", err) + } + + ch := make(chan struct{}) + + out := streams.NewOut(os.Stdout) + _ = out.SetRawTerminal() + in := streams.NewIn(os.Stdin) + _ = in.SetRawTerminal() + defer func() { + out.RestoreTerminal() + in.RestoreTerminal() + in.Close() + }() + + go func() { + _, _ = stdcopy.StdCopy(out, os.Stderr, execResp.Reader) + ch <- struct{}{} + }() + + go func() { + _, _ = io.Copy(execResp.Conn, in) + ch <- struct{}{} + }() + + _ = MonitorTtySize(ctx, out, u.cli, execCreate.ID, true) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-ch: + cancel() + } + + return nil +} + +// RunUpdate executes the update scripts as the dependabot user, blocks until complete. +func (u *Updater) RunUpdate(ctx context.Context, proxyURL string, apiPort int) error { + execCreate, err := u.cli.ContainerExecCreate(ctx, u.containerID, types.ExecConfig{ + AttachStdout: true, + AttachStderr: true, + User: dependabot, + Env: userEnv(proxyURL, apiPort), + Cmd: []string{"/bin/sh", "-c", "bin/run fetch_files && bin/run update_files"}, + }) + if err != nil { + return fmt.Errorf("failed to create exec: %w", err) + } + + execResp, err := u.cli.ContainerExecAttach(ctx, execCreate.ID, types.ExecStartCheck{}) + if err != nil { + return fmt.Errorf("failed to start exec: %w", err) + } + // blocks until update is complete or ctl-c + ch := make(chan struct{}) + go func() { + _, _ = stdcopy.StdCopy(os.Stdout, os.Stderr, execResp.Reader) + ch <- struct{}{} + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case <-ch: + } + + return nil +} + +// Wait blocks until the condition is true. +func (u *Updater) Wait(ctx context.Context, condition container.WaitCondition) error { + wait, errCh := u.cli.ContainerWait(ctx, u.containerID, condition) + select { + case v := <-wait: + if v.StatusCode != 0 { + return fmt.Errorf("updater exited with code: %v", v.StatusCode) + } + case err := <-errCh: + return fmt.Errorf("updater error while waiting: %w", err) + } + return nil +} + +// Close kills and deletes the container and deletes updater mount paths related to the run. +func (u *Updater) Close() error { + defer os.Remove(u.inputPath) + defer os.RemoveAll(u.outputDir) + + return u.cli.ContainerRemove(context.Background(), u.containerID, types.ContainerRemoveOptions{ + Force: true, + }) +} + +// FileFetcherJobFile is the payload passed to file updater containers. +type FileFetcherJobFile struct { + Job model.Job `json:"job"` +} + +func WriteContainerInput(tempDir string, input interface{}) (string, error) { + // create file: + out, err := os.CreateTemp(TempDir(tempDir), "containers-input-*.json") + if err != nil { + return "", fmt.Errorf("creating container input: %w", err) + } + defer out.Close() + fn := out.Name() + + // TODO why does actions require this? + _ = os.Chmod(fn, 0777) + + // fill with json: + if err := json.NewEncoder(out).Encode(input); err != nil { + _ = os.RemoveAll(fn) + return "", fmt.Errorf("writing container input: %w", err) + } + return fn, nil +} + +func SetupOutputDir(tempDir string) (string, error) { + outputDir, err := os.MkdirTemp(TempDir(tempDir), "fetcher-output-") + if err != nil { + return "", fmt.Errorf("creating output tempdir: %w", err) + } + + repoDir := filepath.Join(outputDir, fetcherRepoDir) + if err := os.Mkdir(repoDir, 0777); err != nil { + return "", fmt.Errorf("creating repo tempdir: %w", err) + } + _ = os.Chmod(outputDir, 0777) + return outputDir, nil +} diff --git a/internal/model/job.go b/internal/model/job.go new file mode 100644 index 0000000..c3d7cb8 --- /dev/null +++ b/internal/model/job.go @@ -0,0 +1,86 @@ +package model + +// Job is the data that is passed to the updater. +type Job struct { + PackageManager string `json:"package-manager" yaml:"package-manager"` + AllowedUpdates []Allowed `json:"allowed-updates" yaml:"allowed-updates,omitempty"` + Dependencies []string `json:"dependencies" yaml:"dependencies,omitempty"` + ExistingPullRequests [][]ExistingPR `json:"existing-pull-requests" yaml:"existing-pull-requests,omitempty"` + Experiments Experiment `json:"experiments" yaml:"experiments,omitempty"` + IgnoreConditions []Condition `json:"ignore-conditions" yaml:"ignore-conditions,omitempty"` + LockfileOnly bool `json:"lockfile-only" yaml:"lockfile-only,omitempty"` + RequirementsUpdateStrategy *string `json:"requirements-update-strategy" yaml:"requirements-update-strategy,omitempty"` + SecurityAdvisories []Advisory `json:"security-advisories" yaml:"security-advisories,omitempty"` + SecurityUpdatesOnly bool `json:"security-updates-only" yaml:"security-updates-only,omitempty"` + Source Source `json:"source" yaml:"source"` + UpdateSubdependencies bool `json:"update-subdependencies" yaml:"update-subdependencies,omitempty"` + UpdatingAPullRequest bool `json:"updating-a-pull-request" yaml:"updating-a-pull-request,omitempty"` + VendorDependencies bool `json:"vendor-dependencies" yaml:"vendor-dependencies,omitempty"` + RejectExternalCode bool `json:"reject-external-code" yaml:"reject-external-code,omitempty"` + CommitMessageOptions *CommitOptions `json:"commit-message-options" yaml:"commit-message-options,omitempty"` + CredentialsMetadata []map[string]string `json:"credentials-metadata" yaml:"credentials-metadata,omitempty"` + MaxUpdaterRunTime int `json:"max-updater-run-time" yaml:"max-updater-run-time,omitempty"` +} + +// Source is a reference to some source code +type Source struct { + Provider string `json:"provider" yaml:"provider,omitempty"` + Repo string `json:"repo" yaml:"repo,omitempty"` + Directory string `json:"directory" yaml:"directory,omitempty"` + Branch *string `json:"branch" yaml:"branch,omitempty"` + Commit *string `json:"commit" yaml:"commit,omitempty"` + + Hostname *string `json:"hostname" yaml:"hostname,omitempty"` // Must be provided if APIEndpoint is + APIEndpoint *string `json:"api-endpoint" yaml:"api-endpoint,omitempty"` // Must be provided if Hostname is +} + +type ExistingPR struct { + DependencyName string `json:"dependency-name" yaml:"dependency-name"` + DependencyVersion string `json:"dependency-version" yaml:"dependency-version"` +} + +type Allowed struct { + DependencyType string `json:"dependency-type,omitempty" yaml:"dependency-type,omitempty"` + DependencyName string `json:"dependency-name,omitempty" yaml:"dependency-name,omitempty"` + UpdateType string `json:"update-type,omitempty" yaml:"update-type,omitempty"` +} + +type Condition struct { + DependencyName string `json:"dependency-name" yaml:"dependency-name"` + Source string `json:"source,omitempty" yaml:"source,omitempty"` + VersionRequirement string `json:"version-requirement,omitempty" yaml:"version-requirement,omitempty"` +} + +type Advisory struct { + DependencyName string `json:"dependency-name" yaml:"dependency-name"` + AffectedVersions []string `json:"affected-versions" yaml:"affected-versions"` + PatchedVersions []string `json:"patched-versions" yaml:"patched-versions"` + UnaffectedVersions []string `json:"unaffected-versions" yaml:"unaffected-versions"` +} + +type Dependency struct { + Name string `json:"name"` + PreviousRequirements *[]Requirement `json:"previous-requirements,omitempty" yaml:"previous-requirements,omitempty"` + PreviousVersion string `json:"previous-version,omitempty" yaml:"previous-version,omitempty"` + Requirements []Requirement `json:"requirements"` + Version *string `json:"version" yaml:"version"` +} + +type Requirement struct { + File string `json:"file" yaml:"file"` + Groups []any `json:"groups" yaml:"groups"` + Metadata *map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Requirement *string `json:"requirement" yaml:"requirement"` + Source *RequirementSource `json:"source" yaml:"source"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` + PreviousVersion string `json:"previous-version,omitempty" yaml:"previous-version,omitempty"` +} + +type RequirementSource map[string]any +type Experiment map[string]any + +type CommitOptions struct { + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` + PrefixDevelopment string `json:"prefix-development,omitempty" yaml:"prefix-development,omitempty"` + IncludeScope *string `json:"include-scope,omitempty" yaml:"include-scope,omitempty"` +} diff --git a/internal/model/job_test.go b/internal/model/job_test.go new file mode 100644 index 0000000..0fe52fb --- /dev/null +++ b/internal/model/job_test.go @@ -0,0 +1,360 @@ +package model + +import ( + "reflect" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestInput(t *testing.T) { + var input Input + if err := yaml.Unmarshal([]byte(exampleJob), &input); err != nil { + t.Fatal(err) + } + var input2 map[string]map[string]any + if err := yaml.Unmarshal([]byte(exampleJob), &input2); err != nil { + t.Fatal(err) + } + compareMap(t, "job", input2["job"], input.Job) +} + +func compareMap(t *testing.T, parent string, expected map[string]any, actual interface{}) { + actualType := reflect.TypeOf(actual) + if actualType.Kind() != reflect.Struct { + // Some fields like Experiments are not modeled as structs + // so there's nothing to do here! + return + } + fields := actualType.NumField() + for key, value := range expected { + // Walk the struct and find the field with the yaml tag that matches the map key name. + fieldIndex := -1 + for i := 0; i < fields; i++ { + field := actualType.Field(i) + name := yamlTagCleaner(field.Tag.Get("yaml")) + if key == name { + fieldIndex = i + break + } + } + if fieldIndex < 0 { + t.Errorf("key is not mapped: %s->%s", parent, key) + } else { + // Now we can compare the values to recur into nested maps. + actualValue := reflect.ValueOf(actual).Field(fieldIndex) + + switch expectedValue := value.(type) { + case map[string]any: + // Recurse to find more mismatches. + structField := actualType.Field(fieldIndex) + name := yamlTagCleaner(structField.Tag.Get("yaml")) + compareMap(t, parent+"->"+name, expectedValue, actualValue.Interface()) + case []any: + // Also check structs that are in arrays. + for _, v := range expectedValue { + switch v := v.(type) { + case map[string]any: + structField := actualType.Field(fieldIndex) + name := yamlTagCleaner(structField.Tag.Get("yaml")) + compareMap(t, parent+"->"+name, v, actualValue.Interface()) + } + } + default: + // Values not matching isn't really a huge concern, but we've come this far. + compareValues(t, parent, key, expectedValue, actualValue) + } + } + } +} + +func compareValues(t *testing.T, parent, key string, expected any, actual reflect.Value) { + if expected == nil && actual.IsNil() { + return + } + if actual.Kind() == reflect.Pointer { + actual = actual.Elem() + } + if !reflect.DeepEqual(expected, actual.Interface()) { + t.Errorf("values are not equal: %s->%s expected %v got %v", parent, key, expected, actual.Interface()) + } +} + +func yamlTagCleaner(tag string) string { + return strings.ReplaceAll(tag, ",omitempty", "") +} + +const exampleJob = `--- +job: + package-manager: npm_and_yarn + source: + provider: github + repo: dependabot/dependabot-core + directory: "/npm_and_yarn/helpers" + branch: + api-endpoint: https://api.github.com/ + hostname: github.com + dependencies: + - got + existing-pull-requests: + - - dependency-name: npm + dependency-version: 6.14.0 + - - dependency-name: prettier + dependency-version: 2.0.1 + - - dependency-name: semver + dependency-version: 7.2.1 + - - dependency-name: semver + dependency-version: 7.2.2 + - - dependency-name: semver + dependency-version: 7.3.0 + - - dependency-name: prettier + dependency-version: 2.0.5 + - - dependency-name: jest + dependency-version: 25.5.0 + - - dependency-name: jest + dependency-version: 25.5.3 + - - dependency-name: jest + dependency-version: 25.5.4 + - - dependency-name: jest + dependency-version: 26.0.0 + - - dependency-name: npm + dependency-version: 6.14.5 + - - dependency-name: jest + dependency-version: 26.0.1 + - - dependency-name: eslint + dependency-version: 7.0.0 + - - dependency-name: eslint + dependency-version: 7.1.0 + - - dependency-name: npm + dependency-version: 6.14.6 + - - dependency-name: lodash + dependency-version: 4.17.19 + - - dependency-name: eslint + dependency-version: 7.6.0 + - - dependency-name: npm + dependency-version: 6.14.7 + - - dependency-name: jest + dependency-version: 26.3.0 + - - dependency-name: jest + dependency-version: 26.4.1 + - - dependency-name: prettier + dependency-version: 2.1.0 + - - dependency-name: eslint + dependency-version: 7.8.0 + - - dependency-name: eslint + dependency-version: 7.13.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 1.0.11 + - - dependency-name: prettier + dependency-version: 2.2.0 + - - dependency-name: eslint + dependency-version: 7.19.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.2.5 + - - dependency-name: eslint + dependency-version: 7.21.0 + - - dependency-name: npm + dependency-version: 7.6.1 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.2.7 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.2.8 + - - dependency-name: semver + dependency-version: 7.3.5 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.4.4 + - - dependency-name: detect-indent + dependency-version: 6.1.0 + - - dependency-name: prettier + dependency-version: 2.3.1 + - - dependency-name: jest + dependency-version: 27.0.6 + - - dependency-name: detect-indent + dependency-version: 7.0.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.8.1 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.8.2 + - - dependency-name: npm + dependency-version: 6.14.15 + - - dependency-name: jest + dependency-version: 27.1.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.8.3 + - - dependency-name: jest + dependency-version: 27.1.1 + - - dependency-name: prettier + dependency-version: 2.4.0 + - - dependency-name: jest + dependency-version: 27.2.0 + - - dependency-name: jest + dependency-version: 27.2.1 + - - dependency-name: jest + dependency-version: 27.2.2 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.9.0 + - - dependency-name: jest + dependency-version: 27.2.3 + - - dependency-name: "@npmcli/arborist" + dependency-version: 2.10.0 + - - dependency-name: npm + dependency-version: 8.0.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.0.0 + - - dependency-name: eslint + dependency-version: 8.0.1 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.0.1 + - - dependency-name: npm + dependency-version: 8.1.0 + - - dependency-name: jest + dependency-version: 27.3.0 + - - dependency-name: jest + dependency-version: 27.3.1 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.0.2 + - - dependency-name: npm + dependency-version: 8.1.1 + - - dependency-name: eslint + dependency-version: 8.1.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.0.3 + - - dependency-name: npm + dependency-version: 8.1.2 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.0.4 + - - dependency-name: npm + dependency-version: 8.1.3 + - - dependency-name: eslint + dependency-version: 8.2.0 + - - dependency-name: npm + dependency-version: 8.1.4 + - - dependency-name: prettier + dependency-version: 2.5.0 + - - dependency-name: jest + dependency-version: 27.4.0 + - - dependency-name: jest + dependency-version: 27.4.2 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.1.0 + - - dependency-name: eslint + dependency-version: 8.4.0 + - - dependency-name: jest + dependency-version: 27.4.4 + - - dependency-name: jest + dependency-version: 27.4.6 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.1.2 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.2.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.3.0 + - - dependency-name: eslint + dependency-version: 8.8.0 + - - dependency-name: jest + dependency-version: 27.5.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 4.3.1 + - - dependency-name: eslint + dependency-version: 8.9.0 + - - dependency-name: eslint-config-prettier + dependency-version: 8.4.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.0.0 + - - dependency-name: eslint + dependency-version: 8.10.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.0.1 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.0.2 + - - dependency-name: eslint + dependency-version: 8.11.0 + - - dependency-name: prettier + dependency-version: 2.6.1 + - - dependency-name: eslint + dependency-version: 8.14.0 + - - dependency-name: jest + dependency-version: 28.0.0 + - - dependency-name: jest + dependency-version: 28.0.1 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.1.1 + - - dependency-name: jest + dependency-version: 28.0.2 + - - dependency-name: jest + dependency-version: 28.0.3 + - - dependency-name: eslint + dependency-version: 8.16.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.2.1 + - - dependency-name: eslint + dependency-version: 8.17.0 + - - dependency-name: prettier + dependency-version: 2.7.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.2.2 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.3.0 + - - dependency-name: eslint + dependency-version: 8.20.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.4.0 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.5.0 + - - dependency-name: jest + dependency-version: 29.0.1 + - - dependency-name: eslint + dependency-version: 8.23.0 + - - dependency-name: jest + dependency-version: 29.0.2 + - - dependency-name: jest + dependency-version: 29.0.3 + - - dependency-name: detect-indent + dependency-version: 7.0.1 + - - dependency-name: got + dependency-removed: true + - dependency-name: npm + dependency-version: 8.19.2 + - - dependency-name: "@npmcli/arborist" + dependency-version: 5.6.2 + updating-a-pull-request: false + lockfile-only: false + update-subdependencies: false + ignore-conditions: + - dependency-name: npm + version-requirement: + update-types: + - version-update:semver-major + source: ".github/dependabot.yml" + - dependency-name: npm + version-requirement: ">= 7.a, < 8" + update-types: + source: "@dependabot ignore command" + requirements-update-strategy: + allowed-updates: + - dependency-type: direct + update-type: all + credentials-metadata: + - type: git_source + host: github.com + security-advisories: + - dependency-name: got + patched-versions: [] + unaffected-versions: [] + affected-versions: + - "< 11.8.5" + - ">= 12.0.0 < 12.1.0" + max-updater-run-time: 1800 + vendor-dependencies: false + experiments: + build-pull-request-message: true + npm-transitive-dependency-removal: true + npm-transitive-security-updates: true + reject-external-code: false + commit-message-options: + prefix: + prefix-development: + include-scope: + security-updates-only: true +` diff --git a/internal/model/scenario.go b/internal/model/scenario.go new file mode 100644 index 0000000..8246ade --- /dev/null +++ b/internal/model/scenario.go @@ -0,0 +1,25 @@ +package model + +// Scenario is a way to test a job by asserting the outputs. +type Scenario struct { + // Input is the input parameters + Input Input `yaml:"input"` + // Output is the list of expected outputs + Output []Output `yaml:"output,omitempty"` +} + +// Input is the input to a job +type Input struct { + // Job is the data given to the updater + Job Job `yaml:"job"` + // Credentials is the registry info and tokens to pass to the Proxy + Credentials []map[string]string `yaml:"credentials,omitempty"` +} + +// Output is the expected output given the inputs +type Output struct { + // Type is the kind of data to be checked, e.g. update_dependency_list, create_pull_request, etc + Type string `yaml:"type"` + // Expect is the data expected to be sent + Expect UpdateWrapper `yaml:"expect"` +} diff --git a/internal/model/update.go b/internal/model/update.go new file mode 100644 index 0000000..4d39d27 --- /dev/null +++ b/internal/model/update.go @@ -0,0 +1,53 @@ +package model + +type UpdateWrapper struct { + Data any `json:"data" yaml:"data"` +} + +type UpdateDependencyList struct { + Dependencies []Dependency `json:"dependencies" yaml:"dependencies"` + DependencyFiles []string `json:"dependency_files" yaml:"dependency_files"` +} + +type CreatePullRequest struct { + BaseCommitSha string `json:"base-commit-sha" yaml:"base-commit-sha"` + Dependencies []Dependency `json:"dependencies" yaml:"dependencies"` + UpdatedDependencyFiles []DependencyFile `json:"updated-dependency-files" yaml:"updated-dependency-files"` + PRTitle string `json:"pr-title" yaml:"pr-title,omitempty"` + PRBody string `json:"pr-body" yaml:"pr-body,omitempty"` + CommitMessage string `json:"commit-message" yaml:"commit-message,omitempty"` +} + +type UpdatePullRequest struct { + BaseCommitSha string `json:"base-commit-sha" yaml:"base-commit-sha"` + DependencyNames []string `json:"dependency-names" yaml:"dependency-names"` + UpdatedDependencyFiles []DependencyFile `json:"updated-dependency-files" yaml:"updated-dependency-files"` + PRTitle string `json:"pr-title" yaml:"pr-title,omitempty"` + PRBody string `json:"pr-body" yaml:"pr-body,omitempty"` + CommitMessage string `json:"commit-message" yaml:"commit-message,omitempty"` +} + +type DependencyFile struct { + Content string `json:"content" yaml:"content"` + ContentEncoding string `json:"content_encoding" yaml:"content_encoding"` + Deleted bool `json:"deleted" yaml:"deleted"` + Directory string `json:"directory" yaml:"directory"` + Name string `json:"name" yaml:"name"` + Operation string `json:"operation" yaml:"operation"` + SupportFile bool `json:"support_file" yaml:"support_file"` + Type string `json:"type" yaml:"type"` +} + +type ClosePullRequest struct { + DependencyNames []string `json:"dependency-names" yaml:"dependency-names"` + Reason string `json:"reason" yaml:"reason"` +} + +type MarkAsProcessed struct { + BaseCommitSha string `json:"base-commit-sha" yaml:"base-commit-sha"` +} + +type RecordPackageManagerVersion struct { + Ecosystem string `json:"ecosystem" yaml:"ecosystem"` + PackageManagers map[string]any `json:"package-managers" yaml:"package-managers"` +} diff --git a/internal/server/api.go b/internal/server/api.go new file mode 100644 index 0000000..ad609e8 --- /dev/null +++ b/internal/server/api.go @@ -0,0 +1,247 @@ +package server + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "runtime" + "strings" + "time" + + "github.com/sergi/go-diff/diffmatchpatch" + + "github.com/dependabot/cli/internal/model" + "gopkg.in/yaml.v3" +) + +// API intercepts calls to the Dependabot API +type API struct { + // Expectations is the list of expectations that haven't been met yet + Expectations []model.Output + // Errors is the error list populated by doing a Dependabot run + Errors []error + // Actual will contain the scenario output that actually happened after the run is Complete + Actual model.Scenario + + server *http.Server + cursor int + hasExpectations bool + port int +} + +// NewAPI creates a new API instance and starts the server +func NewAPI(expected []model.Output) *API { + fakeAPIHost := "127.0.0.1" + if runtime.GOOS == "linux" { + fakeAPIHost = "0.0.0.0" + } + if os.Getenv("FAKE_API_HOST") != "" { + fakeAPIHost = os.Getenv("FAKE_API_HOST") + } + // Bind to port 0 for arbitrary port assignment + l, err := net.Listen("tcp", fakeAPIHost+":0") + if err != nil { + panic(err) + } + server := &http.Server{ + ReadTimeout: 5 * time.Second, + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + } + api := &API{ + server: server, + Expectations: expected, + cursor: 0, + hasExpectations: len(expected) > 0, + port: l.Addr().(*net.TCPAddr).Port, + } + server.Handler = api + + go func() { + if err := server.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal(err) + } + }() + + return api +} + +// Port returns the port the API is listening on +func (a *API) Port() int { + return a.port +} + +// Stop stops the server +func (a *API) Stop() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + _ = a.server.Shutdown(ctx) + cancel() +} + +// Complete adds any remaining expectations to the error queue +func (a *API) Complete() { + for i := a.cursor; i < len(a.Expectations); i++ { + exp := &a.Expectations[i] + a.Errors = append(a.Errors, fmt.Errorf("expectation not met: %v\n%v", exp.Type, exp.Expect)) + } +} + +// ServeHTTP handles requests to the server +func (a *API) ServeHTTP(_ http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + err = fmt.Errorf("failed to read body: %w", err) + a.pushError(err) + return + } + if err = r.Body.Close(); err != nil { + err = fmt.Errorf("failed to close body: %w", err) + a.pushError(err) + return + } + + parts := strings.Split(r.URL.String(), "/") + kind := parts[len(parts)-1] + + if err := a.pushResult(kind, data); err != nil { + a.pushError(err) + return + } + + if !a.hasExpectations { + return + } + + a.assertExpectation(kind, data) +} + +func (a *API) assertExpectation(kind string, actualData []byte) { + if len(a.Expectations) <= a.cursor { + err := fmt.Errorf("missing expectation") + a.pushError(err) + return + } + expect := &a.Expectations[a.cursor] + a.cursor++ + if kind != expect.Type { + err := fmt.Errorf("type was unexpected: expected %v got %v", expect.Type, kind) + a.pushError(err) + } + expectJSON, _ := json.Marshal(expect.Expect) + // pretty both for sorting and can use simple comparison + prettyData, _ := pretty(string(expectJSON)) + actual, _ := pretty(string(actualData)) + if actual != prettyData { + err := fmt.Errorf("expected output doesn't match actual data received") + a.pushError(err) + + // print diff to stdout + dmp := diffmatchpatch.New() + + const checklines = false + diffs := dmp.DiffMain(prettyData, actual, checklines) + + diffs = dmp.DiffCleanupSemantic(diffs) + + fmt.Println(dmp.DiffPrettyText(diffs)) + } +} + +func (a *API) pushError(err error) { + escapedError := strings.ReplaceAll(err.Error(), "\n", "") + escapedError = strings.ReplaceAll(escapedError, "\r", "") + log.Println(escapedError) + a.Errors = append(a.Errors, err) +} + +func (a *API) pushResult(kind string, data []byte) error { + actual, err := decodeWrapper(kind, data) + if err != nil { + return err + } + // TODO validate required data + output := model.Output{ + Type: kind, + Expect: *actual, + } + a.Actual.Output = append(a.Actual.Output, output) + + if msg, ok := actual.Data.(model.MarkAsProcessed); ok { + // record the commit SHA so the test is reproducible + a.Actual.Input.Job.Source.Commit = &msg.BaseCommitSha + } + + return nil +} + +// pretty indents and sorts the keys for a consistent comparison +func pretty(jsonString string) (string, error) { + var v map[string]any + if err := json.Unmarshal([]byte(jsonString), &v); err != nil { + return "", err + } + removeNullsFromObjects(v) + // shouldn't be possible to error + b, _ := json.MarshalIndent(v, "", " ") + return string(b), nil +} + +func removeNullsFromObjects(m map[string]any) { + for k, v := range m { + switch assertedVal := v.(type) { + case nil: + delete(m, k) + case map[string]any: + removeNullsFromObjects(assertedVal) + case []any: + for _, item := range assertedVal { + switch assertedItem := item.(type) { + case map[string]any: + removeNullsFromObjects(assertedItem) + } + } + } + } +} + +func decodeWrapper(kind string, data []byte) (*model.UpdateWrapper, error) { + var actual model.UpdateWrapper + switch kind { + case "update_dependency_list": + actual.Data = decode[model.UpdateDependencyList](data) + case "create_pull_request": + actual.Data = decode[model.CreatePullRequest](data) + case "update_pull_request": + actual.Data = decode[model.UpdatePullRequest](data) + case "close_pull_request": + actual.Data = decode[model.ClosePullRequest](data) + case "mark_as_processed": + actual.Data = decode[model.MarkAsProcessed](data) + case "record_package_manager_version": + actual.Data = decode[model.RecordPackageManagerVersion](data) + case "record_update_job_error": + actual.Data = decode[map[string]any](data) + default: + return nil, fmt.Errorf("unexpected output type: %s", kind) + } + return &actual, nil +} + +func decode[T any](data []byte) any { + var wrapper struct { + Data T `json:"data" yaml:"data"` + } + err := yaml.NewDecoder(bytes.NewBuffer(data)).Decode(&wrapper) + if err != nil { + panic(err) + } + return wrapper.Data +} diff --git a/internal/server/input.go b/internal/server/input.go new file mode 100644 index 0000000..4001783 --- /dev/null +++ b/internal/server/input.go @@ -0,0 +1,45 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/dependabot/cli/internal/model" +) + +type credServer struct { + server *http.Server + data *model.Input +} + +// the server receives one payload and shuts itself down +func (s *credServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&s.data); err != nil { + panic(err) + } + w.WriteHeader(200) + _ = r.Body.Close() + go func() { + _ = s.server.Shutdown(context.Background()) + }() +} + +// Input receives configuration via HTTP on the port and returns it decoded +func Input(port int) *model.Input { + server := &http.Server{ + Addr: fmt.Sprintf("127.0.0.1:%d", port), + ReadHeaderTimeout: time.Second, + } + s := &credServer{server: server} + server.Handler = s + // printing so the user doesn't think the cli is hanging + log.Println("waiting for input on port", port) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + panic(err) + } + return s.data +} diff --git a/internal/server/input_test.go b/internal/server/input_test.go new file mode 100644 index 0000000..24e6fc2 --- /dev/null +++ b/internal/server/input_test.go @@ -0,0 +1,37 @@ +package server + +import ( + "bytes" + "net/http" + "sync" + "testing" + + "github.com/dependabot/cli/internal/model" +) + +func TestInput(t *testing.T) { + wg := sync.WaitGroup{} + wg.Add(1) + var input *model.Input + go func() { + input = Input(8080) + wg.Done() + }() + + data := `{"job":{"package-manager":"test"},"credentials":[{"credential":"value"}]}` + resp, err := http.Post("http://localhost:8080", "application/json", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err.Error()) + } + if resp.StatusCode != 200 { + t.Errorf("expected status code 200, got %d", resp.StatusCode) + } + wg.Wait() + + if input.Job.PackageManager != "test" { + t.Errorf("expected package manager to be 'test', got '%s'", input.Job.PackageManager) + } + if input.Credentials[0]["credential"] != "value" { + t.Errorf("expected credential to be 'value', got '%v'", input.Credentials[0]) + } +} diff --git a/script/download-cache.sh b/script/download-cache.sh new file mode 100755 index 0000000..d98feb0 --- /dev/null +++ b/script/download-cache.sh @@ -0,0 +1,16 @@ +#!/bin/bash +if [ $# -eq 0 ] + then + echo "First argument is the ecosystem cache name, e.g. bundler" + exit 1 +fi +retry=0 +until [ "$retry" -ge 5 ] +do + gh run download --repo dependabot/smoke-tests --name cache-"$1" --dir cache && exit 0 + retry=$((retry+1)) + sleep 1 +done + +# Failed to download cache after all retries +exit 1 diff --git a/script/run-all.sh b/script/run-all.sh new file mode 100755 index 0000000..e3b3423 --- /dev/null +++ b/script/run-all.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# This script is useful for regenerating all of the smoke tests running locally. + +declare -a arr=("actions" "bundler" "cargo" "composer" "docker" "elm" "go" "gradle" "hex" "maven" "npm" "nuget" "pip" "pip-compile" "pipenv" "poetry" "pub" "submodules" "terraform") +for eco in "${arr[@]}" +do + go run cmd/dependabot/dependabot.go test -f "testdata/smoke-$eco.yaml" -o "testdata/smoke-$eco.yaml" +done diff --git a/testdata/go/allow-go.yaml b/testdata/go/allow-go.yaml new file mode 100644 index 0000000..1a9e795 --- /dev/null +++ b/testdata/go/allow-go.yaml @@ -0,0 +1,112 @@ +input: + job: + package-manager: go_modules + allowed-updates: + - dependency-name: github.com/fatih/color + source: + provider: github + repo: dependabot/smoke-tests + directory: / + commit: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 +output: + - type: update_dependency_list + expect: + data: + dependencies: + - name: github.com/fatih/color + requirements: + - file: go.mod + groups: [] + requirement: v1.7.0 + source: + source: github.com/fatih/color + type: default + version: 1.7.0 + - name: rsc.io/qr + requirements: [] + version: 0.1.0 + - name: rsc.io/quote + requirements: + - file: go.mod + groups: [] + requirement: v1.4.0 + source: + source: rsc.io/quote + type: default + version: 1.4.0 + dependency_files: + - /go.mod + - /go.sum + - type: create_pull_request + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 + dependencies: + - name: github.com/fatih/color + previous-requirements: + - file: go.mod + groups: [] + requirement: v1.7.0 + source: + source: github.com/fatih/color + type: default + previous-version: 1.7.0 + requirements: + - file: go.mod + groups: [] + requirement: 1.13.0 + source: + source: github.com/fatih/color + type: default + version: 1.13.0 + updated-dependency-files: + - content: | + module github.com/dependabot/vgotest + + go 1.12 + + require ( + github.com/fatih/color v1.13.0 + golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect + rsc.io/qr v0.1.0 + rsc.io/quote v1.4.0 + ) + + replace rsc.io/qr => github.com/rsc/qr v0.2.0 + content_encoding: utf-8 + deleted: false + directory: / + name: go.mod + operation: update + support_file: false + type: file + - content: | + github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= + github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= + github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= + github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= + github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= + github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= + github.com/rsc/qr v0.2.0 h1:tH61+huiZvu+hXL1VUovAu2AnhdG4eJQk2+on3XsDBQ= + github.com/rsc/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= + golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= + golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= + golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= + rsc.io/quote v1.4.0 h1:tYuJspOzwTRMUOX6qmSDRTEKFVV80GM0/l89OLZuVNg= + rsc.io/quote v1.4.0/go.mod h1:S2vMDfxMfk+OGQ7xf1uNqJCSuSPCW5QC127LHYfOJmQ= + rsc.io/sampler v1.0.0 h1:CZX0Ury6np11Lwls9Jja2rFf3YrNPeUPAWiEVrJ0u/4= + rsc.io/sampler v1.0.0/go.mod h1:cqxpM3ZVz9VtirqxZPmrWzkQ+UkiNiGtkrN+B+i8kx8= + content_encoding: utf-8 + deleted: false + directory: / + name: go.sum + operation: update + support_file: false + type: file + - type: mark_as_processed + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 diff --git a/testdata/go/close-pr.yaml b/testdata/go/close-pr.yaml new file mode 100644 index 0000000..c5cfa85 --- /dev/null +++ b/testdata/go/close-pr.yaml @@ -0,0 +1,51 @@ +input: + job: + package-manager: go_modules + dependencies: + - none + security-updates-only: true + source: + provider: github + repo: dependabot/smoke-tests + directory: / + commit: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 + updating-a-pull-request: true +output: + - type: update_dependency_list + expect: + data: + dependencies: + - name: github.com/fatih/color + requirements: + - file: go.mod + groups: [] + requirement: v1.7.0 + source: + source: github.com/fatih/color + type: default + version: 1.7.0 + - name: rsc.io/qr + requirements: [] + version: 0.1.0 + - name: rsc.io/quote + requirements: + - file: go.mod + groups: [] + requirement: v1.4.0 + source: + source: rsc.io/quote + type: default + version: 1.4.0 + dependency_files: + - /go.mod + - /go.sum + - type: close_pull_request + expect: + data: + dependency-names: + - none + reason: dependency_removed + - type: mark_as_processed + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 diff --git a/testdata/go/security-go.yaml b/testdata/go/security-go.yaml new file mode 100644 index 0000000..a6eae9c --- /dev/null +++ b/testdata/go/security-go.yaml @@ -0,0 +1,117 @@ +input: + job: + package-manager: go_modules + allowed-updates: + - update-type: all + security-advisories: + - dependency-name: github.com/fatih/color + affected-versions: + - <1.10.0 + patched-versions: [] + unaffected-versions: [] + security-updates-only: true + source: + provider: github + repo: dependabot/smoke-tests + directory: / + commit: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 +output: + - type: update_dependency_list + expect: + data: + dependencies: + - name: github.com/fatih/color + requirements: + - file: go.mod + groups: [] + requirement: v1.7.0 + source: + source: github.com/fatih/color + type: default + version: 1.7.0 + - name: rsc.io/qr + requirements: [] + version: 0.1.0 + - name: rsc.io/quote + requirements: + - file: go.mod + groups: [] + requirement: v1.4.0 + source: + source: rsc.io/quote + type: default + version: 1.4.0 + dependency_files: + - /go.mod + - /go.sum + - type: create_pull_request + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 + dependencies: + - name: github.com/fatih/color + previous-requirements: + - file: go.mod + groups: [] + requirement: v1.7.0 + source: + source: github.com/fatih/color + type: default + previous-version: 1.7.0 + requirements: + - file: go.mod + groups: [] + requirement: 1.13.0 + source: + source: github.com/fatih/color + type: default + version: 1.10.0 + updated-dependency-files: + - content: | + module github.com/dependabot/vgotest + + go 1.12 + + require ( + github.com/fatih/color v1.10.0 + golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect + rsc.io/qr v0.1.0 + rsc.io/quote v1.4.0 + ) + + replace rsc.io/qr => github.com/rsc/qr v0.2.0 + content_encoding: utf-8 + deleted: false + directory: / + name: go.mod + operation: update + support_file: false + type: file + - content: | + github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= + github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= + 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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= + github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= + github.com/rsc/qr v0.2.0 h1:tH61+huiZvu+hXL1VUovAu2AnhdG4eJQk2+on3XsDBQ= + github.com/rsc/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= + golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= + golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= + golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= + rsc.io/quote v1.4.0 h1:tYuJspOzwTRMUOX6qmSDRTEKFVV80GM0/l89OLZuVNg= + rsc.io/quote v1.4.0/go.mod h1:S2vMDfxMfk+OGQ7xf1uNqJCSuSPCW5QC127LHYfOJmQ= + rsc.io/sampler v1.0.0 h1:CZX0Ury6np11Lwls9Jja2rFf3YrNPeUPAWiEVrJ0u/4= + rsc.io/sampler v1.0.0/go.mod h1:cqxpM3ZVz9VtirqxZPmrWzkQ+UkiNiGtkrN+B+i8kx8= + content_encoding: utf-8 + deleted: false + directory: / + name: go.sum + operation: update + support_file: false + type: file + - type: mark_as_processed + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 diff --git a/testdata/go/update-pr.yaml b/testdata/go/update-pr.yaml new file mode 100644 index 0000000..a915093 --- /dev/null +++ b/testdata/go/update-pr.yaml @@ -0,0 +1,108 @@ +input: + job: + package-manager: go_modules + allowed-updates: + - update-type: all + dependencies: + - rsc.io/quote + existing-pull-requests: + - - dependency-name: rsc.io/quote + dependency-version: 1.5.2 + ignore-conditions: + - dependency-name: github.com/fatih/color + source: tests/go/update-pr.yaml + - dependency-name: rsc.io/quote + source: tests/go/update-pr.yaml + version-requirement: '>1.9.0' + source: + provider: github + repo: dependabot/smoke-tests + directory: / + commit: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 + updating-a-pull-request: true +output: + - type: update_dependency_list + expect: + data: + dependencies: + - name: github.com/fatih/color + requirements: + - file: go.mod + groups: [] + requirement: v1.7.0 + source: + source: github.com/fatih/color + type: default + version: 1.7.0 + - name: rsc.io/qr + requirements: [] + version: 0.1.0 + - name: rsc.io/quote + requirements: + - file: go.mod + groups: [] + requirement: v1.4.0 + source: + source: rsc.io/quote + type: default + version: 1.4.0 + dependency_files: + - /go.mod + - /go.sum + - type: update_pull_request + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 + dependency-names: + - rsc.io/quote + updated-dependency-files: + - content: | + module github.com/dependabot/vgotest + + go 1.12 + + require ( + github.com/fatih/color v1.7.0 + github.com/mattn/go-colorable v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect + rsc.io/qr v0.1.0 + rsc.io/quote v1.5.2 + ) + + replace rsc.io/qr => github.com/rsc/qr v0.2.0 + content_encoding: utf-8 + deleted: false + directory: / + name: go.mod + operation: update + support_file: false + type: file + - content: | + github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= + github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= + github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= + github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= + github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= + github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= + github.com/rsc/qr v0.2.0 h1:tH61+huiZvu+hXL1VUovAu2AnhdG4eJQk2+on3XsDBQ= + github.com/rsc/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= + golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= + golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= + golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= + golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= + rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= + rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= + rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= + rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= + content_encoding: utf-8 + deleted: false + directory: / + name: go.sum + operation: update + support_file: false + type: file + - type: mark_as_processed + expect: + data: + base-commit-sha: 832e37c1a7a4ef89feb9dc7cfa06f62205191994 diff --git a/testdata/private-bundler.json b/testdata/private-bundler.json new file mode 100644 index 0000000..5ebf682 --- /dev/null +++ b/testdata/private-bundler.json @@ -0,0 +1,22 @@ +{ + "job": { + "package-manager": "bundler", + "allowed-updates": [ + { + "update-type": "all" + } + ], + "source": { + "provider": "github", + "repo": "dsp-testing/ruby-bundler-private-registry", + "directory": "/" + } + }, + "credentials": [ + { + "type": "rubygems_server", + "host": "https://rubygems.pkg.github.com/dsp-testing", + "token": "$GPR_TOKEN" + } + ] +} diff --git a/testdata/private-npm.yaml b/testdata/private-npm.yaml new file mode 100644 index 0000000..66f467c --- /dev/null +++ b/testdata/private-npm.yaml @@ -0,0 +1,12 @@ +job: + package-manager: npm_and_yarn + allowed-updates: + - update-type: all + source: + provider: github + repo: dsp-testing/npm-private-registry + directory: /consumer +credentials: + - type: npm_registry + registry: https://npm.pkg.github.com + token: $GPR_TOKEN