From a16a541449b179192122c80228f7161a963fc211 Mon Sep 17 00:00:00 2001 From: Chandru Pokala Date: Thu, 17 Nov 2022 14:44:47 -0800 Subject: [PATCH] Initial commit Co-authored-by: Ahsan Khan Co-authored-by: Ang Zhou Co-authored-by: Anqi Pang Co-authored-by: Hsing-Yu Chen Co-authored-by: Justin Alvarez Co-authored-by: Kevin Li Co-authored-by: Monirul Islam Co-authored-by: Mrudul Harwani Co-authored-by: Sam Berning Co-authored-by: Vishwas Siravara Co-authored-by: Weike Qu Co-authored-by: Ziwen Ning --- .github/ISSUE_TEMPLATE/bug_report.md | 28 + .github/ISSUE_TEMPLATE/config.yaml | 6 + .github/ISSUE_TEMPLATE/feature_request.md | 19 + .github/PULL_REQUEST_TEMPLATE.md | 14 + .github/bin/update-lima-dep.sh | 31 + .github/dependabot.yaml | 21 + .github/workflows/ci.yaml | 118 +++ .github/workflows/lint-pr-title.yaml | 20 + .github/workflows/release-please.yaml | 45 + .github/workflows/update-deps.yaml | 40 + .github/workflows/upload-build-to-s3.yaml | 101 +++ .gitignore | 14 + .golangci.yaml | 66 ++ CONTRIBUTING.md | 187 ++++ LICENSE | 202 +++++ Makefile | 270 ++++++ NOTICE | 2 + README.md | 74 ++ cmd/main.go | 121 +++ cmd/main_test.go | 146 +++ cmd/nerdctl.go | 167 ++++ cmd/nerdctl_test.go | 272 ++++++ cmd/version.go | 31 + cmd/version_test.go | 48 + cmd/virtual_machine.go | 88 ++ cmd/virtual_machine_init.go | 106 +++ cmd/virtual_machine_init_test.go | 298 ++++++ cmd/virtual_machine_remove.go | 71 ++ cmd/virtual_machine_remove_test.go | 166 ++++ cmd/virtual_machine_start.go | 96 ++ cmd/virtual_machine_start_test.go | 311 +++++++ cmd/virtual_machine_stop.go | 69 ++ cmd/virtual_machine_stop_test.go | 165 ++++ cmd/virtual_machine_test.go | 136 +++ config.yaml | 3 + copyright_header | 2 + e2e/config_test.go | 118 +++ e2e/e2e_test.go | 98 ++ e2e/version_test.go | 21 + e2e/virtual_machine_test.go | 58 ++ finch.yaml | 187 ++++ go.mod | 61 ++ go.sum | 847 ++++++++++++++++++ networks.yaml | 29 + pkg/command/command.go | 31 + pkg/command/exec.go | 57 ++ pkg/command/exec_test.go | 55 ++ pkg/command/exit_error.go | 36 + pkg/command/exit_error_test.go | 58 ++ pkg/command/lima.go | 134 +++ pkg/command/lima_test.go | 317 +++++++ pkg/config/config.go | 130 +++ pkg/config/config_test.go | 239 +++++ pkg/config/defaults.go | 42 + pkg/config/defaults_test.go | 95 ++ pkg/config/lima_config_applier.go | 57 ++ pkg/config/lima_config_applier_test.go | 102 +++ pkg/config/nerdctl_config_applier.go | 156 ++++ pkg/config/nerdctl_config_applier_test.go | 243 +++++ pkg/config/validate.go | 56 ++ pkg/config/validate_test.go | 112 +++ pkg/dependency/dependency.go | 89 ++ pkg/dependency/dependency_test.go | 209 +++++ pkg/dependency/vmnet/binaries.go | 131 +++ pkg/dependency/vmnet/binaries_test.go | 256 ++++++ pkg/dependency/vmnet/sudoers_file.go | 84 ++ pkg/dependency/vmnet/sudoers_file_test.go | 194 ++++ .../vmnet/update_override_lima_config.go | 127 +++ .../vmnet/update_override_lima_config_test.go | 280 ++++++ pkg/dependency/vmnet/vmnet.go | 57 ++ pkg/dependency/vmnet/vmnet_test.go | 31 + pkg/flog/level_string.go | 24 + pkg/flog/log.go | 30 + pkg/flog/logrus.go | 71 ++ pkg/fmemory/fmemory.go | 27 + pkg/fssh/fssh.go | 67 ++ pkg/fssh/fssh_test.go | 141 +++ pkg/lima/lima.go | 52 ++ pkg/lima/lima_test.go | 94 ++ pkg/mocks/command_command.go | 130 +++ pkg/mocks/command_command_creator.go | 57 ++ pkg/mocks/command_lima_cmd_creator.go | 93 ++ pkg/mocks/finch_finder_deps.go | 99 ++ pkg/mocks/lima_cmd_creator_system_deps.go | 108 +++ pkg/mocks/logger.go | 197 ++++ pkg/mocks/pkg_config_lima_config_applier.go | 51 ++ pkg/mocks/pkg_config_load_system_deps.go | 51 ++ .../pkg_config_nerdctl_config_applier.go | 51 ++ ...nfig_nerdctl_config_applier_system_deps.go | 51 ++ pkg/mocks/pkg_dependency_dependency.go | 79 ++ pkg/mocks/pkg_fmemory_memory.go | 51 ++ pkg/mocks/pkg_ssh_dialer.go | 53 ++ pkg/path/finch.go | 81 ++ pkg/path/finch_test.go | 128 +++ pkg/system/stdlib.go | 59 ++ pkg/system/system.go | 68 ++ pkg/tools.go | 16 + pkg/version/version.go | 8 + 98 files changed, 10288 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100755 .github/bin/update-lima-dep.sh create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/lint-pr-title.yaml create mode 100644 .github/workflows/release-please.yaml create mode 100644 .github/workflows/update-deps.yaml create mode 100644 .github/workflows/upload-build-to-s3.yaml create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NOTICE create mode 100644 README.md create mode 100644 cmd/main.go create mode 100644 cmd/main_test.go create mode 100644 cmd/nerdctl.go create mode 100644 cmd/nerdctl_test.go create mode 100644 cmd/version.go create mode 100644 cmd/version_test.go create mode 100644 cmd/virtual_machine.go create mode 100644 cmd/virtual_machine_init.go create mode 100644 cmd/virtual_machine_init_test.go create mode 100644 cmd/virtual_machine_remove.go create mode 100644 cmd/virtual_machine_remove_test.go create mode 100644 cmd/virtual_machine_start.go create mode 100644 cmd/virtual_machine_start_test.go create mode 100644 cmd/virtual_machine_stop.go create mode 100644 cmd/virtual_machine_stop_test.go create mode 100644 cmd/virtual_machine_test.go create mode 100644 config.yaml create mode 100644 copyright_header create mode 100644 e2e/config_test.go create mode 100644 e2e/e2e_test.go create mode 100644 e2e/version_test.go create mode 100644 e2e/virtual_machine_test.go create mode 100644 finch.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 networks.yaml create mode 100644 pkg/command/command.go create mode 100644 pkg/command/exec.go create mode 100644 pkg/command/exec_test.go create mode 100644 pkg/command/exit_error.go create mode 100644 pkg/command/exit_error_test.go create mode 100644 pkg/command/lima.go create mode 100644 pkg/command/lima_test.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/defaults.go create mode 100644 pkg/config/defaults_test.go create mode 100644 pkg/config/lima_config_applier.go create mode 100644 pkg/config/lima_config_applier_test.go create mode 100644 pkg/config/nerdctl_config_applier.go create mode 100644 pkg/config/nerdctl_config_applier_test.go create mode 100644 pkg/config/validate.go create mode 100644 pkg/config/validate_test.go create mode 100644 pkg/dependency/dependency.go create mode 100644 pkg/dependency/dependency_test.go create mode 100644 pkg/dependency/vmnet/binaries.go create mode 100644 pkg/dependency/vmnet/binaries_test.go create mode 100644 pkg/dependency/vmnet/sudoers_file.go create mode 100644 pkg/dependency/vmnet/sudoers_file_test.go create mode 100644 pkg/dependency/vmnet/update_override_lima_config.go create mode 100644 pkg/dependency/vmnet/update_override_lima_config_test.go create mode 100644 pkg/dependency/vmnet/vmnet.go create mode 100644 pkg/dependency/vmnet/vmnet_test.go create mode 100644 pkg/flog/level_string.go create mode 100644 pkg/flog/log.go create mode 100644 pkg/flog/logrus.go create mode 100644 pkg/fmemory/fmemory.go create mode 100644 pkg/fssh/fssh.go create mode 100644 pkg/fssh/fssh_test.go create mode 100644 pkg/lima/lima.go create mode 100644 pkg/lima/lima_test.go create mode 100644 pkg/mocks/command_command.go create mode 100644 pkg/mocks/command_command_creator.go create mode 100644 pkg/mocks/command_lima_cmd_creator.go create mode 100644 pkg/mocks/finch_finder_deps.go create mode 100644 pkg/mocks/lima_cmd_creator_system_deps.go create mode 100644 pkg/mocks/logger.go create mode 100644 pkg/mocks/pkg_config_lima_config_applier.go create mode 100644 pkg/mocks/pkg_config_load_system_deps.go create mode 100644 pkg/mocks/pkg_config_nerdctl_config_applier.go create mode 100644 pkg/mocks/pkg_config_nerdctl_config_applier_system_deps.go create mode 100644 pkg/mocks/pkg_dependency_dependency.go create mode 100644 pkg/mocks/pkg_fmemory_memory.go create mode 100644 pkg/mocks/pkg_ssh_dialer.go create mode 100644 pkg/path/finch.go create mode 100644 pkg/path/finch_test.go create mode 100644 pkg/system/stdlib.go create mode 100644 pkg/system/system.go create mode 100644 pkg/tools.go create mode 100644 pkg/version/version.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..6c4adeafe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug Report +about: Report a bug to help improve Finch +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +Briefly describe the probem you are having. + + +**Steps to reproduce** +A clear, step-by-step set of instructions to reproduce the bug. + + +**Expected behavior** +Description of what you expected to happen. + + +**Screenshots or logs** +If applicable, add screenshots or logs to help explain your problem. + + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 000000000..021507c67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/runfinch/finch/discussions + about: Use GitHub Discussions to ask questions, discuss options, or propose new ideas + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..5c9fbdaed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Suggest a new feature for Finch +title: '' +labels: 'feature' +assignees: '' + +--- + +**What is the problem you're trying to solve?.** +A clear and concise description of the use case for this feature. Please provide an example, if possible. + + +**Describe the feature you'd like** +A clear and concise description of what you'd like to happen. + + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..748ae5771 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +Issue #, if available: + +*Description of changes:* + +*Testing done:* + + + +- [ ] I've reviewed the guidance in CONTRIBUTING.md + + +#### License Acceptance + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/bin/update-lima-dep.sh b/.github/bin/update-lima-dep.sh new file mode 100755 index 000000000..ea5081682 --- /dev/null +++ b/.github/bin/update-lima-dep.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +CLOUDFRONT_URL="https://deps.runfinch.com/" +AARCH64_FILENAME_PATTERN="aarch64/lima-and-qemu.macos-aarch64.[0-9].*\.gz$" +AMD64_FILENAME_PATTERN="x86-64/lima-and-qemu.macos-x86_64.[0-9].*\.gz$" +AARCH64="aarch64" +X86_64="x86-64" +set -x + +while getopts b: flag +do + case "${flag}" in + b) bucket=${OPTARG};; + esac +done +[[ -z "$bucket" ]] && { echo "Error: Bucket not set"; exit 1; } + +aarch64Deps=$(aws s3 ls s3://${bucket}/${AARCH64}/ --recursive | grep "$AARCH64_FILENAME_PATTERN" | sort | tail -n 1 | awk '{print $4}') + +[[ -z "$aarch64Deps" ]] && { echo "Error: aarch64 dependency not found"; exit 1; } + + +amd64Deps=$(aws s3 ls s3://${bucket}/${X86_64}/ --recursive | grep "$AMD64_FILENAME_PATTERN" | sort | tail -n 1 | awk '{print $4}') + +[[ -z "$amd64Deps" ]] && { echo "Error: x86_64 dependency not found"; exit 1; } + +sed -E -i.bak 's|^([[:blank:]]*LIMA_URL[[:blank:]]*\?=[[:blank:]]*'${CLOUDFRONT_URL}')('${AARCH64_FILENAME_PATTERN}')|\1'$aarch64Deps'|' Makefile +sed -E -i.bak 's|^([[:blank:]]*LIMA_URL[[:blank:]]*\?=[[:blank:]]*'${CLOUDFRONT_URL}')('${AMD64_FILENAME_PATTERN}')|\1'$amd64Deps'|' Makefile + + + diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..7bac0735c --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,21 @@ +version: 2 +updates: +- package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + commit-message: + # When a dependency is updated, + # we want release-please to treat the corresponding commit as a releasable unit + # because it may contain a security fix. + # + # Re. how that is achieved, see `changelog-types` in workflows/release-please.yml. + prefix: "build" + include: "scope" +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: "ci" + include: "scope" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..0a3c354fc --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,118 @@ +# When a third-party action is added (i.e., `uses`), please also add it to `download-licenses` in Makefile. +name: CI +on: + push: + branches: + - main + paths-ignore: + - '*.md' + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + gen-code-no-diff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version-file: go.mod + cache: true + - run: make gen-code + - run: git diff --exit-code + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + # Since this repository is not meant to be used as a library, + # we don't need to test the latest 2 major releases like Go does: https://go.dev/doc/devel/release#policy. + go-version-file: go.mod + cache: true + - run: make test-unit + # It's recommended to run golangci-lint in a job separate from other jobs (go test, etc) because different jobs run in parallel. + go-linter: + runs-on: ubuntu-latest + # TODO: Remove this when we make the repos public. + env: + GOPRIVATE: github.com/runfinch/common-tests + ACCESS_TOKEN: ${{ secrets.FINCH_BOT_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version-file: go.mod + cache: true + # TODO: Remove this when we make the repos public. + - run: git config --global url.https://$ACCESS_TOKEN@github.com/.insteadOf https://github.com/ + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Pin the version in case all the builds start to fail at the same time. + # There may not be an automatic way (e.g., dependabot) to update a specific parameter of a Github Action, + # so we will just update it manually whenever it makes sense (e.g., a feature that we want is added). + version: v1.50.0 + args: --fix=false + go-mod-tidy-check: + runs-on: ubuntu-latest + # TODO: Remove this when we make the repos public. + env: + GOPRIVATE: github.com/runfinch/common-tests + ACCESS_TOKEN: ${{ secrets.FINCH_BOT_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version-file: go.mod + cache: true + # TODO: Remove this when we make the repos public. + - run: git config --global url.https://$ACCESS_TOKEN@github.com/.insteadOf https://github.com/ + # TODO: Use `go mod tidy --check` after https://github.com/golang/go/issues/27005 is fixed. + - run: go mod tidy + - run: git diff --exit-code + check-licenses: + runs-on: ubuntu-latest + # TODO: Remove this when we make the repos public. + env: + GOPRIVATE: github.com/runfinch/common-tests + ACCESS_TOKEN: ${{ secrets.FINCH_BOT_TOKEN }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version-file: go.mod + cache: true + # TODO: Remove this when we make the repos public. + - run: git config --global url.https://$ACCESS_TOKEN@github.com/.insteadOf https://github.com/ + - run: make check-licenses + e2e-tests: + strategy: + fail-fast: false + matrix: + os: [[self-hosted, macos, amd64, 11.7], [self-hosted, macos, amd64, 12.6], [self-hosted, macos, arm64, 11.7], [self-hosted, macos, arm64, 12.6]] + runs-on: ${{ matrix.os }} + # TODO: Remove this when we make the repos public + env: + ACCESS_TOKEN: ${{ secrets.FINCH_BOT_TOKEN }} + steps: + - uses: actions/checkout@v3 + - name: Clean up previous files + run: | + sudo rm -rf /opt/finch + sudo rm -rf ~/.finch + - run: brew install go lz4 automake autoconf libtool + - name: Build project + run: | + export PATH="/opt/homebrew/opt/libtool/libexec/gnubin:$PATH" + which libtool + make + # TODO: Remove this when we make the repos public + - name: Configure repo access + run: git config --global url.https://$ACCESS_TOKEN@github.com/.insteadOf https://github.com/ + - run: make test-e2e diff --git a/.github/workflows/lint-pr-title.yaml b/.github/workflows/lint-pr-title.yaml new file mode 100644 index 000000000..9671c4da8 --- /dev/null +++ b/.github/workflows/lint-pr-title.yaml @@ -0,0 +1,20 @@ +# When a third-party action is added (i.e., `uses`), please also add it to `download-licenses` in Makefile. +name: "Lint PR Title" + +on: + # TODO: Change to pull_request_target after the repo is public. + pull_request: + types: + - opened + - edited + - reopened + - synchronize + +jobs: + main: + name: conventional-commit + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml new file mode 100644 index 000000000..359e51cac --- /dev/null +++ b/.github/workflows/release-please.yaml @@ -0,0 +1,45 @@ +# When a third-party action is added (i.e., `uses`), please also add it to `download-licenses` in Makefile. +on: + push: + branches: + - main +name: release-please +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + with: + release-type: go + package-name: finch + # Include 'build' in the changelog and + # make it a releasable unit (patch version bump) because dependabot PRs uses it. + # For more details, see ../dependabot.yml. + # + # The mapping from type to section comes from conventional-commit-types [1] + # which is used by action-semantic-pull-request [2], + # which is used by us. + # + # [1] https://github.com/commitizen/conventional-commit-types/blob/master/index.json + # [2] https://github.com/amannn/action-semantic-pull-request/blob/0b14f54ac155d88e12522156e52cb6e397745cfd/README.md?plain=1#L60 + changelog-types: > + [ + { + "type":"feat", + "section":"Features", + "hidden":false + }, + { + "type":"fix", + "section":"Bug Fixes", + "hidden":false + }, + { + "type":"build", + "section":"Build System or External Dependencies", + "hidden":false + } + ] + # Before we are at v1.0.0 + bump-minor-pre-major: true + diff --git a/.github/workflows/update-deps.yaml b/.github/workflows/update-deps.yaml new file mode 100644 index 000000000..0702b9a11 --- /dev/null +++ b/.github/workflows/update-deps.yaml @@ -0,0 +1,40 @@ +name: Update dependencies +on: + workflow_dispatch: + +permissions: + # This is required for configure-aws-credentials to request an OIDC JWT ID token to access AWS resources later on. + # More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings + id-token: write + contents: write + pull-requests: write + +jobs: + update-deps: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ secrets.ROLE }} + role-session-name: dependency-upload-session + aws-region: ${{ secrets.REGION }} + + # This step fetches the latest set of released dependencies from s3 and updates the Makefile to use the same. + - name: update dependencies url + run: | + ./.github/bin/update-lima-dep.sh -b ${{ secrets.DEPENDENCY_BUCKET_NAME }} + + - name: create PR + uses: peter-evans/create-pull-request@v4 + with: + # A Personal Access Token instead of the default `GITHUB_TOKEN` is required + # to trigger the checks (e.g., e2e tests) on the created pull request. + # More info: https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#workarounds-to-trigger-further-workflow-runs + # TODO: Use FINCH_BOT_TOKEN instead of GITHUB_TOKEN. + token: ${{ secrets.GITHUB_TOKEN }} + # TODO: Add updated lima version in the title. + title: 'build(deps): Bump lima version' diff --git a/.github/workflows/upload-build-to-s3.yaml b/.github/workflows/upload-build-to-s3.yaml new file mode 100644 index 000000000..3288f8738 --- /dev/null +++ b/.github/workflows/upload-build-to-s3.yaml @@ -0,0 +1,101 @@ +name: Upload build to s3 + +on: + workflow_dispatch: +env: + GO111MODULE: on + +permissions: + # This is required for configure-aws-credentials to request an OIDC JWT ID token to access AWS resources later on. + # More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings + id-token: write + contents: read # This is required for actions/checkout + +jobs: + macos-arm64-build: + runs-on: ['self-hosted', 'macos', 'arm64', '11.7'] + timeout-minutes: 60 + steps: + - uses: actions/setup-go@v2 + with: + go-version: 1.19.x + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Make macos arm64 build + run: | + make clean + make download-licenses + make FINCH_OS_IMAGE_LOCATION_ROOT=/Applications/Finch + tar -zcvf finch.arm64."$(date '+%s').tar.gz" _output + + - name: Upload macos arm64 build + uses: actions/upload-artifact@v2 + with: + name: finch.macos-arm64 + path: finch.arm64.*.tar.gz + if-no-files-found: error + + macos-x86-build: + runs-on: ['self-hosted', 'macos', 'amd64', '11.7'] + timeout-minutes: 60 + steps: + - uses: actions/setup-go@v2 + with: + go-version: 1.19.x + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Make macos amd64 build + run: | + make clean + make download-licenses + make FINCH_OS_IMAGE_LOCATION_ROOT=/Applications/Finch + tar -zcvf finch.amd64."$(date '+%s').tar.gz" _output + + - name: Upload macos amd64 build + uses: actions/upload-artifact@v2 + with: + name: finch.macos-amd64 + path: finch.amd64.*.tar.gz + if-no-files-found: error + + release: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: + - macos-x86-build + - macos-arm64-build + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + persist-credentials: false + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ secrets.ROLE }} + role-session-name: dependency-upload-session + aws-region: ${{ secrets.REGION }} + + - name: Download macos arm64 build + uses: actions/download-artifact@v2 + with: + name: finch.macos-arm64 + path: build + + - name: Download macos amd64 build + uses: actions/download-artifact@v2 + with: + name: finch.macos-amd64 + path: build + # TODO: Change destination bucket after creating automation for signing. + - name: "Upload to S3" + run: | + aws s3 cp ./build/ s3://${{ secrets.DEPENDENCY_BUCKET_NAME }}/aarch64/ --recursive --exclude "*" --include "finch.arm64*" + aws s3 cp ./build/ s3://${{ secrets.DEPENDENCY_BUCKET_NAME }}/x86-64/ --recursive --exclude "*" --include "finch.amd64*" diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..740718608 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +_output/ +*.idea +git-diff +*.DS_Store +*.bak +_vde_output/ +**/*.tar.gz +**/*.lz4 +**/*.img +tmp/ +.vscode/ +finch-core/ +tools_bin/ +test-coverage.* diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 000000000..bced172d4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,66 @@ +# The sections in this file are ordered in the order presented in https://golangci-lint.run/usage/configuration/. +# The nested fields are ordered alphabetically. + +linters-settings: + goheader: + template-path: copyright_header + goimports: + local-prefixes: github.com/runfinch/finch + gosec: + config: + G306: "0o644" + lll: + # 145 is just a lax value that does not require too much work to add this check, + # and we don't want this to be too strict anyway. + line-length: 145 + tab-width: 4 + makezero: + always: true + nolintlint: + require-explanation: true + require-specific: true + stylecheck: + # ST1003 is left out because it is a bit opinionated. + checks: ["all", "-ST1003"] +linters: + enable: + - errname + - errorlint + - exportloopref + - forcetypeassert + - gocritic + - godot + - gofumpt + - goheader + - goimports + - gosec + - lll + - makezero + - misspell + - nilerr + - nilnil + - nolintlint + - nosprintfhostport + - paralleltest + - predeclared + - reassign + - revive + - testableexamples + - unconvert + - unparam + - usestdlibvars + # TODO: Enable wastedassign after https://github.com/sanposhiho/wastedassign/issues/41 is fixed. + # - wastedassign + - whitespace + - stylecheck +issues: + exclude-rules: + - linters: + - lll + # A go:generate statement has to be in the same line: https://github.com/golang/go/issues/46050. + source: "^//go:generate " + # Some checks enabled in the stylecheck setting are disabled by default + # (e.g., https://golangci-lint.run/usage/false-positives/#exc0013), + # so we need to enable them explicitly here. + exclude-use-default: false + fix: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..f248123b0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,187 @@ +# Contributing Guide + +* [New Contributor Guide](#contributing-guide) + * [Ways to Contribute](#ways-to-contribute) + * [Find an Issue](#find-an-issue) + * [Ask for Help](#ask-for-help) + * [Pull Request Lifecycle](#pull-request-lifecycle) + * [Development Environment Setup](#development-environment-setup) + * [Sign Your Commits](#sign-your-commits) + * [Pull Request Checklist](#pull-request-checklist) + +Welcome! We are glad that you want to contribute to our project! 💖 + +As you get started, you are in the best position to give us feedback on areas of +our project that we need help with including: + +* Problems found during setting up a new developer environment +* Gaps in our Quickstart Guide or documentation +* Bugs in our automation scripts + +If anything doesn't make sense, or doesn't work when you run it, please open a +bug report and let us know! + +Thanks to the maintainers of the [CNCF Project Template Repository](https://github.com/cncf/project-template) for the great work they have done. + +## Participating in the Project + +There are a number of ways to participate in this project. As the project evolves and grows, we will define a more formal governance model. For now, this document describes various ways community members might participate. + +### Community Participant + +A Community Participant engages with the project and its community, contributing their time, thoughts, etc. Community participants are usually users who have stopped being anonymous and started being active in project discussions. + +### Contributor + +A Contributor contributes directly to the project. Contributions need not be code. People at the Contributor level may be new contributors, or they may only contribute occasionally. + +### Maintainer + +Maintainers are established contributors who are responsible for the entire project. As such, they have the ability to approve PRs against any area of the project, and are expected to participate in making decisions about the strategy and priorities of the project. + +## Ways to Contribute + +We welcome many different types of contributions including: + +* New features +* Builds, CI/CD +* Bug fixes +* Documentation +* Issue Triage +* Communications / Social Media / Blog Posts +* Release management + +## Find an Issue + +We have good first issues for new contributors and help wanted issues suitable +for any contributor. [good first issue](TODO) has extra information to +help you make your first contribution. [help wanted](TODO) are issues +suitable for someone who isn't a core maintainer and is good to move onto after +your first pull request. + +Sometimes there won’t be any issues with these labels. That’s ok! There is +likely still something for you to work on. If you want to contribute but you +don’t know where to start or have an idea, feel free to open a new issue in Github for brainstorming. + +Once you see an issue that you'd like to work on, please post a comment saying +that you want to work on it. Something like "I want to work on this" is fine. + +## Ask for Help + +The best way to reach us with a question when contributing is to ask on the original github issue. + +## Pull Request Lifecycle + +Generally a comment should be resolved by the one who leaves the comment. + +For PR authors, if a comment is not left by you, please do not resolve it even after applying the changes suggested by it. This is to make sure that the changes do address the concern of the PR reviewer as there could be misunderstanding between PR authors and PR reviewers. However, if the PR reviewer is not responding to the comment for whatever reason, the project maintainers can help resolve the comment to unblock the PR author. + +For PR reviewers, after a comment left by you is acted upon, it is encouraged to either reply to it or resolve it in a timely manner to unblock the PR author because all the comments are required to be resolved before a PR can be merged. For project maintainers, please target handling unresolved comments within 2 working days. + +We feel spelling these norms out is better than assuming them, and we all acknowledge life happens and these are guidelines, not strict rules. + + +## Development Environment Setup + +### Linter + +We use [golangci-lint](https://github.com/golangci/golangci-lint). + +To integrate it into your IDE, please check out the [official documentation](https://golangci-lint.run/usage/integrations/). + +For more details, see [`.golangci.yaml`](./.golangci.yaml) and the `lint` target in [`Makefile`](./Makefile). + +### Build + +After cloning the repo, run `make` to build the binary. + +The binary in _output can be directly used. E.g. initializing the vm and display the version +``` +./_output/bin/finch vm init\ + +./_output/bin/finch version +``` + +You can run `make install` to make finch binary globally accessible. + +## Sign Your Commits + +### DCO +Licensing is important to open source projects. It provides some assurances that +the software will continue to be available based under the terms that the +author(s) desired. We require that contributors sign off on commits submitted to +our project's repositories. The [Developer Certificate of Origin +(DCO)](https://probot.github.io/apps/dco/) is a way to certify that you wrote and +have the right to contribute the code you are submitting to the project. + +You sign-off by adding the following to your commit messages. Your sign-off must +match the git user and email associated with the commit. + + This is my commit message + + Signed-off-by: Your Name + +Git has a `-s` command line option to do this automatically: + + git commit -s -m 'This is my commit message' + +If you forgot to do this and have not yet pushed your changes to the remote +repository, you can amend your commit with the sign-off by running + + git commit --amend -s + +## Pull Request Checklist + +When you submit your pull request, or you push new commits to it, our automated +systems will run some checks on your new code. We require that your pull request +passes these checks, but we also have more criteria than just that before we can +accept and merge it. We recommend that you check the following things locally +before you submit your code: + +### Build + +```make``` + +### Lint +```make lint``` + +### Testing + +#### Unit Testing - Parallel by Default + +```make test-unit``` + +For each unit test case (i.e., in both `TestXXX` and the function passed to `t.Run`), `t.Parallel` should be added by default. It should only be skipped under special situations (e.g., `T.Setenv` is used in that test). + +Rationale: + +- Each unit test case should be independent from each other, so they should be able to be executed in parallel. +- Adding a `t.Parallel` is not much effort as all the underlying details are handled by Go std lib. +- `t.Parallel` helps us ensure that the test cases are truly independent from each other. +- The running time can (theoretically) only go down. + +Keeping a good unit test coverage will be part of pull request review. You can run `make coverage` to self-check the coverage. + +#### E2E Testing + +```make test-e2e``` + +See `test-e2e` section in [`Makefile`](./Makefile) for more reference. + +If the e2e test scenarios you are going to contribute + +- are in generic container development workflow +- can be shared by finch-core by replacing test subject from "finch" to "limactl ..." +- E.g.: pull, push, build, run, etc. + +implement them in common-tests repo and then import them in [`./e2e/e2e_test.go`](./e2e/e2e_test.go) in finch CLI and finch-core. The detailed flow can be found [here](https://github.com/runfinch/common-tests#sync-between-tests-and-code). + +Otherwise, it means that the scenarios are specific to finch CLI (e.g., version, VM lifecycle, etc.), and you should implement them under `./e2e/` (e.g., `./e2e/version.go`) and import them in `./e2e/e2e_test.go`. + +### Go File Naming + +Keep file names to one word if possible (e.g., avoid stuttering with package name: prefer `thing/factory.go` over `thing/thing_factory.go`). If there have to be more than one words, use underscores as separators. Do not use hyphens or camelCase. + +Rationale: It's more readable (i.e., `complicateddistirbutedsystem` vs `complicated_distributed_system`). Furthermore, the practical reason to avoid underscores as separators is that the suffix may later become either an OS or an architecture, but we think that the potential risk is outweighed by the readability gain. + +To add more context, there are some [public discussions](https://github.com/golang/go/issues/36060#issue-535213527) on this, but there is no consensus yet. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a81ca43da --- /dev/null +++ b/Makefile @@ -0,0 +1,270 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +GO ?= go +# Files are installed under $(DESTDIR)/$(PREFIX) +PREFIX ?= $(CURDIR)/_output +DEST := $(shell echo "$(DESTDIR)/$(PREFIX)" | sed 's:///*:/:g; s://*$$::') +BINDIR ?= /usr/local/bin +OUTDIR ?= $(CURDIR)/_output +PACKAGE := github.com/runfinch/finch +BINARYNAME := finch +LIMA_FILENAME := lima +LIMA_EXTENSION := .tar.gz + +LIMA_HOME := $(DEST)/lima/data +# Created by the CLI after installation, only used in uninstall step +LIMA_VDE_SUDOERS_FILE := /etc/sudoers.d/finch-lima +# Final installation prefix for vde created by CLI after installation, only used in uninstall step +VDE_INSTALL ?= /opt/finch +UNAME := $(shell uname -m) +ARCH ?= $(UNAME) +SUPPORTED_ARCH = false +CORE_URL ?= https://artifact.runfinch.com/finch-core-0.1.0.tar.gz +CORE_FILENAME := finch-core +CORE_OUTDIR := $(CURDIR)/$(CORE_FILENAME)/_output +CORE_VDE_PREFIX ?= $(OUTDIR)/dependencies/vde/opt/finch +LICENSEDIR := $(OUTDIR)/license-files +VERSION := $(shell git describe --match 'v[0-9]*' --dirty='.modified' --always --tags) +LDFLAGS := "-X $(PACKAGE)/pkg/version.Version=$(VERSION)" + +.DEFAULT_GOAL := all + +ifneq (,$(findstring arm64,$(ARCH))) + SUPPORTED_ARCH = true + LIMA_ARCH = aarch64 + # From https://dl.fedoraproject.org/pub/fedora/linux/releases/37/Cloud/aarch64/images/ + FINCH_OS_BASENAME ?= Fedora-Cloud-Base-37-1.7.aarch64.qcow2 + LIMA_URL ?= https://deps.runfinch.com/aarch64/lima-and-qemu.macos-aarch64.1668543750.tar.gz +else ifneq (,$(findstring x86_64,$(ARCH))) + SUPPORTED_ARCH = true + LIMA_ARCH = x86_64 + # From https://dl.fedoraproject.org/pub/fedora/linux/releases/37/Cloud/x86_64/images/ + FINCH_OS_BASENAME ?= Fedora-Cloud-Base-37-1.7.x86_64.qcow2 + LIMA_URL ?= https://deps.runfinch.com/x86-64/lima-and-qemu.macos-x86_64.1668543664.tar.gz +endif + +FINCH_OS_HASH := `shasum -a 256 $(OUTDIR)/os/$(FINCH_OS_BASENAME) | cut -d ' ' -f 1` +FINCH_OS_DIGEST := "sha256:$(FINCH_OS_HASH)" +FINCH_OS_IMAGE_LOCATION_ROOT ?= $(DEST) +FINCH_OS_IMAGE_LOCATION ?= $(FINCH_OS_IMAGE_LOCATION_ROOT)/os/$(FINCH_OS_BASENAME) + +.PHONY: arch-test +arch-test: + @if [ $(SUPPORTED_ARCH) != "true" ]; then echo "Unsupported architecture: $(ARCH)"; exit "1"; fi + +.PHONY: all +all: arch-test finch finch-core finch.yaml networks.yaml config.yaml lima-and-qemu + +.PHONY: finch-core +finch-core: + mkdir -p $(CURDIR)/$(CORE_FILENAME) + curl -L $(CORE_URL) > "$(CURDIR)/$(CORE_FILENAME).tar.gz" + tar -zvxf $(CURDIR)/finch-core.tar.gz -C $(CORE_FILENAME) --strip-component=1 + rm "$(CORE_FILENAME).tar.gz" + + cd $(CURDIR)/$(CORE_FILENAME) && \ + FINCH_OS_x86_URL="$(FINCH_OS_x86_URL)" \ + FINCH_OS_AARCH64_URL="$(FINCH_OS_AARCH64_URL)" \ + VDE_TEMP_PREFIX=$(CORE_VDE_PREFIX) \ + $(MAKE) + + mkdir -p _output + cd $(CORE_FILENAME)/_output && tar c * | tar Cvx $(OUTDIR) + rm -r $(CURDIR)/$(CORE_FILENAME) + rm -rf $(OUTDIR)/lima-template + +.PHONY: lima-and-qemu +lima-and-qemu: networks.yaml + mkdir -p $(OUTDIR)/downloads + # download artifacts + curl -L $(LIMA_URL) > $(OUTDIR)/downloads/lima-and-qemu.tar.gz + + # Untar LIMA + tar -xvf $(OUTDIR)/downloads/lima-and-qemu.tar.gz -C $(OUTDIR)/lima/ + + # Delete downloads + rm -rf $(OUTDIR)/downloads + + +.PHONY: finch.yaml +finch.yaml: finch-core + mkdir -p $(OUTDIR)/os + cp finch.yaml $(OUTDIR)/os + # using -i.bak is very intentional, it allows the following commands to succeed for both GNU / BSD sed + # this sed command uses the alternative separator of "|" because the image location uses "/" + sed -i.bak -e "s||$(FINCH_OS_IMAGE_LOCATION)|g" $(OUTDIR)/os/finch.yaml + sed -i.bak -e "s//$(LIMA_ARCH)/g" $(OUTDIR)/os/finch.yaml + sed -i.bak -e "s//$(FINCH_OS_DIGEST)/g" $(OUTDIR)/os/finch.yaml + rm $(OUTDIR)/os/*.yaml.bak + +.PHONY: networks.yaml +networks.yaml: + # networking configuration + mkdir -p $(OUTDIR)/lima/data/_config/ + cp networks.yaml $(OUTDIR)/lima/data/_config/ + +.PHONY: config.yaml +config.yaml: + cp config.yaml $(OUTDIR)/config.yaml + +.PHONY: copy +copy: + mkdir -p $(DEST) + (cd _output && tar c * | tar Cvx $(DEST) ) + +.PHONY: install +install: copy + sudo ln -sf $(DEST)/bin/finch "$(BINDIR)/finch" + +uninstall.finch: + @test -f "$(BINDIR)/$(BINARYNAME)" || echo "finch not found in $(BINDIR) prefix" + if [ "$$(readlink "$(BINDIR)/$(BINARYNAME)")" = "$(DEST)/bin/$(BINARYNAME)" ]; then sudo rm "$(BINDIR)/$(BINARYNAME)"; fi + -@rm -rf $(DEST)/bin 2>/dev/null || true + -@rm -rf $(DEST)/lima 2>/dev/null || true + -@rm -rf $(DEST)/os 2>/dev/null || true + -@rm -rf $(DEST)/dependencies 2>/dev/null || true + +.PHONY: uninstall.vde +uninstall.vde: + sudo rm -rf $(VDE_INSTALL) + sudo rm -rf $(LIMA_VDE_SUDOERS_FILE) + +.PHONY: uninstall +uninstall: uninstall.finch + +.PHONY: finch +finch: + $(GO) build -ldflags $(LDFLAGS) -o $(OUTDIR)/bin/$(BINARYNAME) $(PACKAGE)/cmd + +.PHONY: release +release: check-licenses all download-licenses + +.PHONY: coverage +coverage: + go test $(shell go list ./... | grep -v e2e) -coverprofile=test-coverage.out + go tool cover -html=test-coverage.out + +.PHONY: download-licenses +# Licenses of all the third-party dependencies must be downloaded in this build target +# because we need to include them in our release for legal reasons. +# These dependencies include Go modules in go.mod, Github Actions in .github/workflows, system-level dependencies (e.g., lima), etc. +# +# Dependencies in pkg/tools.go need to be manually added below. For more details, see the comments of `check-licenses`. +# Note that technically we don't need to explicitly download all of them (e.g., mockgen; see the next paragraph re. lima for more details), +# but it's just easier to have one entry here for each entry in `pkg/tools.go` instead of trying to understand which are not needed. +# +# At the time of writing, technically we don't need to explicitly download lima, +# but we still choose to do so because `go-licenses save` takes care of code-level dependencies, +# while lima is more like a system-level dependency, and it just happens to be also a code-level dependency right now, +# but it could be removed from go.mod one day, so it may be better to make it clear here. +download-licenses: GOBIN = $(CURDIR)/tools_bin +download-licenses: + GOBIN=$(GOBIN) go install github.com/google/go-licenses + $(GOBIN)/go-licenses save ./... --save_path="$(LICENSEDIR)" --force --include_tests + + ### dependencies in tools.go - start ### + + # for github.com/golang/mock/mockgen + mkdir -p "$(LICENSEDIR)/github.com/golang/mock" + curl https://raw.githubusercontent.com/golang/mock/main/LICENSE --output "$(LICENSEDIR)/github.com/golang/mock/LICENSE" + # for github.com/google/go-licenses + mkdir -p "$(LICENSEDIR)/github.com/google/go-licenses" + curl https://raw.githubusercontent.com/google/go-licenses/master/LICENSE --output "$(LICENSEDIR)/github.com/google/go-licenses/LICENSE" + # for golang.org/x/tools/cmd/stringer + mkdir -p "$(LICENSEDIR)/golang.org/x/tools" + curl https://raw.githubusercontent.com/golang/tools/master/LICENSE --output "$(LICENSEDIR)/golang.org/x/tools/LICENSE" + + ### dependencies in tools.go - end ### + + ### dependencies in ci.yaml - start ### + + mkdir -p "$(LICENSEDIR)/github.com/actions/checkout" + curl https://raw.githubusercontent.com/actions/checkout/main/LICENSE --output "$(LICENSEDIR)/github.com/actions/checkout/LICENSE" + mkdir -p "$(LICENSEDIR)/github.com/actions/setup-go" + curl https://raw.githubusercontent.com/actions/setup-go/main/LICENSE --output "$(LICENSEDIR)/github.com/actions/setup-go/LICENSE" + mkdir -p "$(LICENSEDIR)/github.com/golangci/golangci-lint-action" + curl https://raw.githubusercontent.com/golangci/golangci-lint-action/master/LICENSE --output "$(LICENSEDIR)/github.com/golangci/golangci-lint-action/LICENSE" + + ### dependencies in ci.yaml - end ### + + ### dependencies in lint-pr-title.yaml - start ### + + mkdir -p "$(LICENSEDIR)/github.com/amannn/action-semantic-pull-request" + curl https://raw.githubusercontent.com/amannn/action-semantic-pull-request/main/LICENSE --output "$(LICENSEDIR)/github.com/amannn/action-semantic-pull-request/LICENSE" + + ### dependencies in lint-pr-title.yaml - end ### + + ### dependencies in release-please.yaml - start ### + + mkdir -p "$(LICENSEDIR)/github.com/googleapis/release-please" + curl https://raw.githubusercontent.com/googleapis/release-please/main/LICENSE --output "$(LICENSEDIR)/github.com/googleapis/release-please/LICENSE" + + ### dependencies in release-please.yaml - end ### + + ### system-level dependencies - start ### + + mkdir -p "$(LICENSEDIR)/github.com/lima-vm/lima" + curl https://raw.githubusercontent.com/lima-vm/lima/master/LICENSE --output "$(LICENSEDIR)/github.com/lima-vm/lima/LICENSE" + + ### system-level dependencies - end ### + +.PHONY: check-licenses +# TODO: Include deps only imported in testing code after https://github.com/google/go-licenses/issues/62 is fixed. +# +# Guidelines: +# +# - The dependencies in pkg/tools.go are not included, so one has to manually verify the license when a new tool is added. +# According to https://github.com/google/go-licenses#build-tags, +# we could use `GOFLAGS="-tags=tools"` to include the file, but the following error will occur: +# `import "golang.org/x/tools/cmd/stringer" is a program, not an importable package`. +# - There are many allowed licenses, but to keep things easy to follow, below we only list the ones currently used by our dependencies. +# +# Explanations: +# +# - golang.org/x dependencies are ignored because some of them incur the following warning: +# `contains non-Go code that can't be inspected for further dependencies`, which can clutter the output. +# They can be safely ignored because they are still part of the Go Project according to https://pkg.go.dev/golang.org/x, +# so they should be the same as the standard library license-wise. +# - github.com/runfinch/finch is ignored because we don't have to check our own license. +# Moreover, if we don't ignore it, the following error will occur: +# `module github.com/runfinch/finch has empty version, defaults to HEAD. The license URL may be incorrect. Please verify!`. +check-licenses: GOBIN = $(CURDIR)/tools_bin +check-licenses: + go mod download + GOBIN=$(GOBIN) go install github.com/google/go-licenses + $(GOBIN)/go-licenses check --ignore golang.org/x,github.com/runfinch/finch --allowed_licenses Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC,MIT --include_tests ./... + +.PHONY: test-unit +test-unit: + go test $(shell go list ./... | grep -v e2e) -shuffle on -race + +# test-e2e assumes the VM instance doesn't exist, please make sure to remove it before running. +.PHONY: test-e2e +test-e2e: + go test -ldflags $(LDFLAGS) -timeout 60m ./e2e/... -test.v -ginkgo.v + +.PHONY: gen-code +# Since different projects may have different versions of tool binaries, +# GOBIN is introduced to maintain a set of tool binaries dedicated to our project use. +# +# To add a new tool binary to the recipe below, please also checkout out `pkg/tools.go`. +gen-code: GOBIN = $(CURDIR)/tools_bin +gen-code: + GOBIN=$(GOBIN) go install github.com/golang/mock/mockgen + GOBIN=$(GOBIN) go install golang.org/x/tools/cmd/stringer + # Make sure that we are using the tool binaries which are just built to generate code. + PATH=$(GOBIN):$(PATH) go generate ./... + +.PHONY: lint +# To run golangci-lint locally: https://golangci-lint.run/usage/install/#local-installation +lint: + golangci-lint run + +.PHONY: clean +clean: + -@rm -rf $(OUTDIR) 2>/dev/null || true + -@rm -rf $(CORE_FILENAME) 2>/dev/null || true + -@rm ./*.tar.gz 2>/dev/null || true + -@rm ./*.qcow2 2>/dev/null || true + -@rm ./test-coverage.* 2>/dev/null || true diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..133a93fc6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +Finch CLI +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/README.md b/README.md new file mode 100644 index 000000000..b4edb8c1d --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Finch CLI + +## Local Development + +This section describes how one can develop Finch CLI locally on macOS, build it, and then run it to test out the changes. The design ensures that the local development environment is isolated from the homebrew installation (i.e., we should not need to run `make install` to do local development). + +### Dependency Installation + +This section installs the dependencies need to build and run `finch`. `finch` leverages several other pieces of technology to provide the platform elements which include [lima](https://github.com/lima-vm/lima), [qemu](https://github.com/qemu/qemu) and others. The application wraps numerous pieces of technology to provide one cohesive application. + +It only needs to be done when the repository is just cloned. + +```sh +make +``` + +### Build + +After you make some code changes, run the following command under the repository root to build an updated binary. It will generate a binary under `./_output/bin/finch`. + +```sh +make finch +``` + +### Run + +Spin up the VM if you haven't. Note that if it's your time to run `finch vm init`, it may require you to enter your root password because it needs to configure [socket-vmnet](https://github.com/lima-vm/socket_vmnet), which need to be installed to privileged locations. + +```sh +./_output/bin/finch vm init +``` + +Now you can run whatever command you want to test: + +```sh +./_output/bin/finch ... +``` + +#### Config + +A config file at `$USER/.finch/finch.yaml` will be generated on first-run. Currently, this config file has options for system resource limits for the VM that is used to run containers. These default limits are generated dynamically based on the resources available on the host system, but can be changed by manually editing the config file. + +Currently, the options are: + +- CPUs [int]: the amount of cores to dedicate to the VM (must be greater than 0, warning after exceeding max availbe on host) +- Memory [string]: the amount of RAM to dedicate to the VM (must be greater than 0, warning after exceeding max availbe on host) + +For a full list of configuration options, check [the struct here](pkg/config/config.go#L25). + +An example `config.yaml` looks like this: + +```yaml +cpus: 4 +memory: 4GiB +``` + +### Unit Testing + +To run unit test locally, please run `make test-unit`. Please make sure to run the unit tests before pushing the changes. + +Ideally each go file should have a test file ending with `_test.go`, and we should have as much test coverage as we can. + +To check unit test coverage, run `make coverage` under root finch-cli root directory. + +### E2E Testing + +Run these steps at the first time of running e2e tests + +VM instance is not expected to exist before running e2e tests, please make sure to remove it before going into next step: +```sh +./_output/bin/finch vm stop +./_output/bin/finch vm remove +``` +To run e2e test locally, please run `make test-e2e`. Please make sure to run the e2e tests or add new e2e tests before pushing the changes. diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 000000000..ada815d90 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,121 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package main denotes the entry point of finch CLI. +package main + +import ( + "fmt" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/config" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/dependency/vmnet" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/fmemory" + "github.com/runfinch/finch/pkg/fssh" + "github.com/runfinch/finch/pkg/path" + "github.com/runfinch/finch/pkg/system" + + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +const finchRootCmd = "finch" + +func main() { + logger := flog.NewLogrus() + stdLib := system.NewStdLib() + fs := afero.NewOsFs() + mem := fmemory.NewMemory() + if err := xmain(logger, stdLib, fs, stdLib, mem); err != nil { + logger.Fatal(err) + } +} + +func xmain(logger flog.Logger, ffd path.FinchFinderDeps, fs afero.Fs, loadCfgDeps config.LoadSystemDeps, mem fmemory.Memory) error { + fp, err := path.FindFinch(ffd) + if err != nil { + return fmt.Errorf("failed to find the installation path of Finch: %w", err) + } + + fc, err := config.Load(fs, fp.ConfigFilePath(ffd.Env("HOME")), logger, loadCfgDeps, mem) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + return newApp(logger, fp, fs, fc).Execute() +} + +var newApp = func(logger flog.Logger, fp path.Finch, fs afero.Fs, fc *config.Finch) *cobra.Command { + usage := fmt.Sprintf("%v ", finchRootCmd) + rootCmd := &cobra.Command{ + Use: usage, + Short: "Finch: open-source container development tool", + SilenceUsage: true, + SilenceErrors: true, + Version: finchVersion(), + } + // TODO: Decide when to forward --debug to the dependencies + // (e.g. nerdctl for container commands and limactl for VM commands). + rootCmd.PersistentFlags().Bool("debug", false, "running under debug mode") + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + // running commands under debug mode will print out debug logs + debugMode, _ := cmd.Flags().GetBool("debug") + if debugMode { + logger.SetLevel(flog.Debug) + } + return nil + } + + ecc := command.NewExecCmdCreator() + lcc := command.NewLimaCmdCreator(ecc, + logger, + fp.LimaHomePath(), + fp.LimactlPath(), + fp.QEMUBinDir(), + system.NewStdLib(), + ) + + // append nerdctl commands + allCommands := initializeNerdctlCommands(lcc, logger) + // append finch specific commands + allCommands = append(allCommands, + newVersionCommand(), + virtualMachineCommands(logger, fp, lcc, ecc, fs, fc), + ) + + rootCmd.AddCommand(allCommands...) + + return rootCmd +} + +func virtualMachineCommands( + logger flog.Logger, + fp path.Finch, + lcc command.LimaCmdCreator, + ecc *command.ExecCmdCreator, + fs afero.Fs, + fc *config.Finch, +) *cobra.Command { + optionalDepGroups := []*dependency.Group{vmnet.NewDependencyGroup(ecc, lcc, fs, fp, logger)} + + return newVirtualMachineCommand( + lcc, + logger, + optionalDepGroups, + config.NewLimaApplier(fc, fs, fp.LimaOverrideConfigPath()), + config.NewNerdctlApplier(fssh.NewDialer(), fs, fp.LimaSSHPrivateKeyPath(), system.NewStdLib()), + fp, + fs, + ) +} + +func initializeNerdctlCommands(lcc command.LimaCmdCreator, logger flog.Logger) []*cobra.Command { + nerdctlCommandCreator := newNerdctlCommandCreator(lcc, logger) + var allNerdctlCommands []*cobra.Command + for cmdName, cmdDescription := range nerdctlCmds { + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) + } + return allNerdctlCommands +} diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 000000000..052536d40 --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,146 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/runfinch/finch/pkg/config" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/mocks" + "github.com/runfinch/finch/pkg/path" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const configStr = ` +memory: 4GiB +cpus: 8 +` + +//nolint:paralleltest // It may not be a good idea to run main() with other tests in parallel. +func TestMainFunc(_ *testing.T) { + main() +} + +func TestXmain(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.FinchFinderDeps, afero.Fs, *mocks.LoadSystemDeps, *mocks.Memory) + wantErr error + }{ + { + name: "happy path", + wantErr: nil, + mockSvc: func( + logger *mocks.Logger, + ffd *mocks.FinchFinderDeps, + fs afero.Fs, + loadCfgDeps *mocks.LoadSystemDeps, + mem *mocks.Memory, + ) { + require.NoError(t, afero.WriteFile(fs, "/home/.finch/finch.yaml", []byte(configStr), 0o600)) + + ffd.EXPECT().Env("HOME").Return("/home") + ffd.EXPECT().Executable().Return("/bin/path", nil) + ffd.EXPECT().EvalSymlinks("/bin/path").Return("/real/bin/path", nil) + ffd.EXPECT().FilePathJoin("/real/bin/path", "../../").Return("/real") + loadCfgDeps.EXPECT().NumCPU().Return(16) + // 12_884_901_888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + }, + }, + { + name: "failed to find the finch path from path.FindFinch", + wantErr: fmt.Errorf("failed to find the installation path of Finch: %w", + fmt.Errorf("failed to locate the executable that starts this process: %w", errors.New("failed to find executable path")), + ), + mockSvc: func( + _ *mocks.Logger, + ffd *mocks.FinchFinderDeps, + _ afero.Fs, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + ) { + ffd.EXPECT().Executable().Return("", errors.New("failed to find executable path")) + }, + }, + { + name: "failed to load finch config because of invalid YAML", + wantErr: fmt.Errorf("failed to load config: %w", + fmt.Errorf("failed to unmarshal config file, using default values: %w", + &yaml.TypeError{Errors: []string{"line 1: cannot unmarshal !!str `this is...` into config.Finch"}}, + ), + ), + mockSvc: func( + _ *mocks.Logger, + ffd *mocks.FinchFinderDeps, + fs afero.Fs, + _ *mocks.LoadSystemDeps, + _ *mocks.Memory, + ) { + require.NoError(t, afero.WriteFile(fs, "/home/.finch/finch.yaml", []byte("this isn't YAML"), 0o600)) + + ffd.EXPECT().Env("HOME").Return("/home") + ffd.EXPECT().Executable().Return("/bin/path", nil) + ffd.EXPECT().EvalSymlinks("/bin/path").Return("/real/bin/path", nil) + ffd.EXPECT().FilePathJoin("/real/bin/path", "../../").Return("/real") + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + ffd := mocks.NewFinchFinderDeps(ctrl) + logger := mocks.NewLogger(ctrl) + loadCfgDeps := mocks.NewLoadSystemDeps(ctrl) + mem := mocks.NewMemory(ctrl) + fs := afero.NewMemMapFs() + tc.mockSvc(logger, ffd, fs, loadCfgDeps, mem) + err := xmain(logger, ffd, fs, loadCfgDeps, mem) + assert.Equal(t, err, tc.wantErr) + }) + } +} + +func TestNewApp(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + l := mocks.NewLogger(ctrl) + fp := path.Finch("") + fs := afero.NewMemMapFs() + + require.NoError(t, afero.WriteFile(fs, "/real/config.yaml", []byte(configStr), 0o600)) + + cmd := newApp(l, fp, fs, &config.Finch{}) + + assert.Equal(t, cmd.Name(), finchRootCmd) + assert.Equal(t, cmd.Version, finchVersion()) + assert.Equal(t, cmd.SilenceUsage, true) + assert.Equal(t, cmd.SilenceErrors, true) + // confirm the number of command, comprised of nerdctl commands + finch commands (version, vm) + assert.Equal(t, len(cmd.Commands()), len(nerdctlCmds)+2) + + // PersistentPreRunE should set logger level to debug if the debug flag exists. + mockCmd := &cobra.Command{} + mockCmd.Flags().Bool("debug", true, "") + l.EXPECT().SetLevel(flog.Debug) + + require.NoError(t, cmd.PersistentPreRunE(mockCmd, nil)) +} diff --git a/cmd/nerdctl.go b/cmd/nerdctl.go new file mode 100644 index 000000000..291e477a5 --- /dev/null +++ b/cmd/nerdctl.go @@ -0,0 +1,167 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/runfinch/finch/pkg/flog" + + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/lima" +) + +const nerdctlCmdName = "nerdctl" + +type nerdctlCommandCreator struct { + creator command.LimaCmdCreator + logger flog.Logger +} + +func newNerdctlCommandCreator(creator command.LimaCmdCreator, logger flog.Logger) *nerdctlCommandCreator { + return &nerdctlCommandCreator{creator: creator, logger: logger} +} + +func (ncc *nerdctlCommandCreator) create(cmdName string, cmdDesc string) *cobra.Command { + command := &cobra.Command{ + Use: cmdName, + Short: cmdDesc, + // TODO(Ang): Remove it (https://github.com/runfinch/finch/pull/40#issuecomment-1263878146). + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, + // If we don't specify it, and the user issues `finch run -d alpine`, + // the args passed to nerdctlCommand.run will be empty because + // cobra will try to parse `-d alpine` as if alpine is the value of the `-d` flag. + DisableFlagParsing: true, + RunE: newNerdctlCommand(ncc.creator, ncc.logger).runAdapter, + } + + return command +} + +type nerdctlCommand struct { + creator command.LimaCmdCreator + logger flog.Logger +} + +func newNerdctlCommand(creator command.LimaCmdCreator, logger flog.Logger) *nerdctlCommand { + return &nerdctlCommand{creator: creator, logger: logger} +} + +func (nc *nerdctlCommand) runAdapter(cmd *cobra.Command, args []string) error { + return nc.run(cmd.Name(), args) +} + +func (nc *nerdctlCommand) run(cmdName string, args []string) error { + err := nc.assertVMIsRunning(nc.creator, nc.logger) + if err != nil { + return err + } + + var nerdctlArgs []string + for _, arg := range args { + if arg == "--debug" { + // explicitly setting log level to avoid `--debug` flag being interpreted as nerdctl command + nc.logger.SetLevel(flog.Debug) + continue + } + nerdctlArgs = append(nerdctlArgs, arg) + } + + limaArgs := append([]string{"shell", limaInstanceName, nerdctlCmdName, cmdName}, nerdctlArgs...) + + if nc.shouldReplaceForHelp(cmdName, args) { + return nc.creator.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, limaArgs...) + } + + return nc.creator.Create(limaArgs...).Run() +} + +func (nc *nerdctlCommand) assertVMIsRunning(creator command.LimaCmdCreator, logger flog.Logger) error { + // Extra call to check VM before running nerdctl commands. These are the reasons of not doing message replacing + // 1. for the non-help commands, replacing stdout may cause "stdin is not a terminal" error for the commands that need input. + // E.g. finch login. + // 2. an extra call could give us more control about the error messages. Message replacing may fail if upstream + // changes the format of source string, which leads to extra CI validation work. + status, err := lima.GetVMStatus(creator, logger, limaInstanceName) + if err != nil { + return err + } + switch status { + case lima.Nonexistent: + return fmt.Errorf("instance %q does not exist, run `finch %s init` to create a new instance", + limaInstanceName, virtualMachineRootCmd) + case lima.Stopped: + return fmt.Errorf("instance %q is stopped, run `finch %s start` to start the instance", + limaInstanceName, virtualMachineRootCmd) + default: + return nil + } +} + +// shouldReplaceForHelp returns true if we should replace "nerdctl" with "finch" for the output of the given command. +func (nc *nerdctlCommand) shouldReplaceForHelp(cmdName string, args []string) bool { + // The implicit help commands mean that if users input "finch" without any args, it will return the help of it. + // Not all the commands are implicit help commands + implicitHelpCmdSet := sets.NewString("system", "builder", "compose", "container", "image", "network", "volume") + + if len(args) == 0 { + if implicitHelpCmdSet.Has(cmdName) { + return true + } + } + + for _, arg := range args { + if arg == "--help" || arg == "-h" { + return true + } + } + + return false +} + +var nerdctlCmds = map[string]string{ + "build": "Build an image from Dockerfile", + "builder": "Manage builds", + "commit": "Create a new image from a container's changes", + "compose": "Compose", + "container": "Manage containers", + "create": "Create a new container", + "events": "Get real time events from the server", + "exec": "Run a command in a running container", + "history": "Show the history of an image", + "image": "Manage images", + "images": "List images", + "info": "Display system-wide information", + "inspect": "Return low-level information on Docker objects", + "kill": "Kill one or more running containers", + "load": "Load an image from a tar archive or STDIN", + "login": "Log in to a container registry", + "logout": "Log out from a container registry", + "logs": "Fetch the logs of a container", + "network": "Manage networks", + "pause": "Pause all processes within one or more containers", + "port": "List port mappings or a specific mapping for the container", + "ps": "List containers", + "pull": "Pull an image from a registry", + "push": "Push an image or a repository to a registry", + "restart": "Restart one or more containers", + "rm": "Remove one or more containers", + "rmi": "Remove one or more images", + "run": "Run a command in a new container", + "save": "Save one or more images to a tar archive (streamed to STDOUT by default)", + "start": "Start one or more stopped containers", + "stats": "Display a live stream of container(s) resource usage statistics", + "stop": "Stop one or more running containers", + "system": "Manage containerd", + "tag": "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", + "top": "Display the running processes of a container", + "unpause": "Unpause all processes within one or more containers", + "update": "Update configuration of one or more containers", + "volume": "Manage volumes", + "wait": "Block until one or more containers stop, then print their exit codes", +} diff --git a/cmd/nerdctl_test.go b/cmd/nerdctl_test.go new file mode 100644 index 000000000..09e02fb33 --- /dev/null +++ b/cmd/nerdctl_test.go @@ -0,0 +1,272 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/mocks" + + "github.com/runfinch/finch/pkg/flog" +) + +var testStdoutRs = []command.Replacement{ + {Source: "nerdctl", Target: "finch"}, +} + +func TestNerdctlCommandCreator_create(t *testing.T) { + t.Parallel() + + cmd := newNerdctlCommandCreator(nil, nil).create("build", "build description") + assert.Equal(t, cmd.Name(), "build") + assert.Equal(t, cmd.DisableFlagParsing, true) + assert.Equal(t, cmd.FParseErrWhitelist, cobra.FParseErrWhitelist{UnknownFlags: true}) +} + +func TestNerdctlCommand_runAdaptor(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cmd *cobra.Command + args []string + mockSvc func(*mocks.LimaCmdCreator, *mocks.Logger, *gomock.Controller) + }{ + { + name: "happy path", + cmd: &cobra.Command{ + Use: "info", + }, + args: []string{}, + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "info").Return(c) + c.EXPECT().Run() + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + logger := mocks.NewLogger(ctrl) + tc.mockSvc(lcc, logger, ctrl) + + assert.NoError(t, newNerdctlCommand(lcc, logger).runAdapter(tc.cmd, tc.args)) + }) + } +} + +func TestNerdctlCommand_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cmdName string + args []string + wantErr error + mockSvc func(*mocks.LimaCmdCreator, *mocks.Logger, *gomock.Controller) + }{ + { + name: "happy path", + cmdName: "build", + args: []string{"-t", "demo", "."}, + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "build", "-t", "demo", ".").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "stopped VM", + cmdName: "build", + args: []string{"-t", "demo", "."}, + wantErr: fmt.Errorf("instance %q is stopped, run `finch %s start` to start the instance", + limaInstanceName, virtualMachineRootCmd), + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + }, + }, + { + name: "nonexistent VM", + cmdName: "build", + args: []string{"-t", "demo", "."}, + wantErr: fmt.Errorf( + "instance %q does not exist, run `finch %s init` to create a new instance", + limaInstanceName, virtualMachineRootCmd), + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + { + name: "unknown VM status", + cmdName: "build", + args: []string{"-t", "demo", "."}, + wantErr: errors.New("unrecognized system status"), + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + cmdName: "build", + args: []string{"-t", "demo", "."}, + wantErr: errors.New("get status error"), + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), errors.New("get status error")) + }, + }, + { + name: "with --debug flag", + cmdName: "pull", + args: []string{"test:tag", "--debug"}, + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + logger.EXPECT().SetLevel(flog.Debug) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "pull", "test:tag").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with --help flag", + cmdName: "pull", + args: []string{"test:tag", "--help"}, + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + lcc.EXPECT().RunWithReplacingStdout( + testStdoutRs, "shell", limaInstanceName, nerdctlCmdName, "pull", "test:tag", "--help").Return(nil) + }, + }, + { + name: "with --help flag but replacing returns error", + cmdName: "pull", + args: []string{"test:tag", "--help"}, + wantErr: fmt.Errorf("failed to replace"), + mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + lcc.EXPECT().RunWithReplacingStdout( + testStdoutRs, "shell", limaInstanceName, nerdctlCmdName, "pull", "test:tag", "--help"). + Return(fmt.Errorf("failed to replace")) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + logger := mocks.NewLogger(ctrl) + tc.mockSvc(lcc, logger, ctrl) + assert.Equal(t, tc.wantErr, newNerdctlCommand(lcc, logger).run(tc.cmdName, tc.args)) + }) + } +} + +func TestNerdctlCommand_shouldReplaceForHelp(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cmdName string + args []string + mockSvc func(*mocks.LimaCmdCreator, *mocks.Logger, *gomock.Controller) + }{ + { + name: "with --help flag", + cmdName: "pull", + args: []string{"test:tag", "--help"}, + }, + { + name: "with -h", + cmdName: "pull", + args: []string{"test:tag", "-h"}, + }, + { + name: "system returns help", + cmdName: "system", + }, + { + name: "builder returns help", + cmdName: "builder", + }, + { + name: "container returns help", + cmdName: "container", + }, + { + name: "image returns help", + cmdName: "image", + }, + { + name: "network returns help", + cmdName: "network", + }, + { + name: "volume returns help", + cmdName: "volume", + }, + { + name: "compose returns help", + cmdName: "compose", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + logger := mocks.NewLogger(ctrl) + assert.True(t, newNerdctlCommand(lcc, logger).shouldReplaceForHelp(tc.cmdName, tc.args)) + }) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 000000000..e197c8b6e --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func finchVersion() string { + // TODO: Remove hardcoded version after the git can be access through buildtime + return "v0.1.0" +} + +func newVersionCommand() *cobra.Command { + versionCommand := &cobra.Command{ + Use: "version", + Args: cobra.NoArgs, + Short: "Show Finch version information", + RunE: versionAction, + } + + return versionCommand +} + +func versionAction(cmd *cobra.Command, args []string) error { + fmt.Println("Finch version:", finchVersion()) + return nil +} diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 000000000..44490b052 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func captureStdout(t *testing.T, f func()) string { + temp := os.Stdout + read, write, _ := os.Pipe() + os.Stdout = write + + f() + + require.NoError(t, write.Close()) + os.Stdout = temp + + var buffer bytes.Buffer + _, err := io.Copy(&buffer, read) + require.NoError(t, err) + return buffer.String() +} + +func TestVersionCommand(t *testing.T) { + t.Parallel() + + cmd := newVersionCommand() + assert.Equal(t, cmd.Name(), "version") +} + +//nolint:paralleltest // TODO: Add t.Parallel after using dependency injection to pass a io.Writer to newVersionCommand. +func TestVersionAction(t *testing.T) { + mockCmd := newVersionCommand() + versionActionFunc := func() { + require.NoError(t, versionAction(mockCmd, []string{})) + } + + output := captureStdout(t, versionActionFunc) + assert.Contains(t, output, finchVersion()) +} diff --git a/cmd/virtual_machine.go b/cmd/virtual_machine.go new file mode 100644 index 000000000..fb8d4ebd3 --- /dev/null +++ b/cmd/virtual_machine.go @@ -0,0 +1,88 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "strings" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/config" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/path" + + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +const ( + limaInstanceName = "finch" + virtualMachineRootCmd = "vm" +) + +func newVirtualMachineCommand( + limaCmdCreator command.LimaCmdCreator, + logger flog.Logger, + optionalDepGroups []*dependency.Group, + lca config.LimaConfigApplier, + nca config.NerdctlConfigApplier, + fp path.Finch, + fs afero.Fs, +) *cobra.Command { + virtualMachineCommand := &cobra.Command{ + Use: virtualMachineRootCmd, + Short: "Manage the virtual machine lifecycle", + } + + virtualMachineCommand.AddCommand( + newStartVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fs, fp.LimaSSHPrivateKeyPath()), + newStopVMCommand(limaCmdCreator, logger), + newRemoveVMCommand(limaCmdCreator, logger), + newInitVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fp.BaseYamlFilePath(), fs, fp.LimaSSHPrivateKeyPath()), + ) + + return virtualMachineCommand +} + +// Used by the actions that call VM start to ensure that the in-VM config file options are applied after boot. +type postVMStartInitAction struct { + creator command.LimaCmdCreator + logger flog.Logger + fs afero.Fs + privateKeyPath string + nca config.NerdctlConfigApplier +} + +func newPostVMStartInitAction( + logger flog.Logger, + creator command.LimaCmdCreator, + fs afero.Fs, + privateKeyPath string, + nca config.NerdctlConfigApplier, +) *postVMStartInitAction { + return &postVMStartInitAction{creator: creator, logger: logger, fs: fs, privateKeyPath: privateKeyPath, nca: nca} +} + +func (p *postVMStartInitAction) runAdapter(cmd *cobra.Command, args []string) error { + return p.run() +} + +func (p *postVMStartInitAction) run() error { + p.logger.Debugln("Applying guest configuration options") + + sshPortArgs := []string{"ls", "-f", "{{.SSHLocalPort}}", limaInstanceName} + sshPortCmd := p.creator.CreateWithoutStdio(sshPortArgs...) + out, err := sshPortCmd.Output() + if err != nil { + return err + } + portString := strings.TrimSpace(string(out)) + + if portString == "0" { + p.logger.Warnln("SSH port = 0, is the instance running? Not able to apply VM configuration options") + return nil + } + return p.nca.Apply(fmt.Sprintf("127.0.0.1:%v", portString)) +} diff --git a/cmd/virtual_machine_init.go b/cmd/virtual_machine_init.go new file mode 100644 index 000000000..941bbc85d --- /dev/null +++ b/cmd/virtual_machine_init.go @@ -0,0 +1,106 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/config" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/lima" + + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newInitVMCommand( + lcc command.LimaCmdCreator, + logger flog.Logger, + optionalDepGroups []*dependency.Group, + lca config.LimaConfigApplier, + nca config.NerdctlConfigApplier, + baseYamlFilePath string, + fs afero.Fs, + privateKeyPath string, +) *cobra.Command { + initVMCommand := &cobra.Command{ + Use: "init", + Short: "Initialize the virtual machine", + RunE: newInitVMAction(lcc, logger, optionalDepGroups, lca, baseYamlFilePath).runAdapter, + PostRunE: newPostVMStartInitAction(logger, lcc, fs, privateKeyPath, nca).runAdapter, + } + + return initVMCommand +} + +type initVMAction struct { + baseYamlFilePath string + creator command.LimaCmdCreator + logger flog.Logger + optionalDepGroups []*dependency.Group + limaConfigApplier config.LimaConfigApplier +} + +func newInitVMAction( + creator command.LimaCmdCreator, + logger flog.Logger, + optionalDepGroups []*dependency.Group, + lca config.LimaConfigApplier, + baseYamlFilePath string, +) *initVMAction { + return &initVMAction{ + creator: creator, logger: logger, optionalDepGroups: optionalDepGroups, limaConfigApplier: lca, baseYamlFilePath: baseYamlFilePath, + } +} + +func (iva *initVMAction) runAdapter(cmd *cobra.Command, args []string) error { + return iva.run() +} + +func (iva *initVMAction) run() error { + err := iva.assertVMIsNonexistent(iva.creator, iva.logger) + if err != nil { + return err + } + + err = dependency.InstallOptionalDeps(iva.optionalDepGroups, iva.logger) + if err != nil { + iva.logger.Error(fmt.Sprintf("Dependency error: %s", err)) + } + + err = iva.limaConfigApplier.Apply() + if err != nil { + return err + } + + instanceName := fmt.Sprintf("--name=%v", limaInstanceName) + limaCmd := iva.creator.CreateWithoutStdio("start", instanceName, iva.baseYamlFilePath, "--tty=false") + iva.logger.Info("Initializing and starting Finch virtual machine...") + logs, err := limaCmd.CombinedOutput() + if err != nil { + iva.logger.Errorf("Finch virtual machine failed to start, debug logs: %s", logs) + return err + } + iva.logger.Info("Finch virtual machine started successfully") + return nil +} + +func (iva *initVMAction) assertVMIsNonexistent(creator command.LimaCmdCreator, logger flog.Logger) error { + status, err := lima.GetVMStatus(creator, logger, limaInstanceName) + if err != nil { + return err + } + switch status { + case lima.Stopped: + return fmt.Errorf( + "the instance %q already exists but is stopped, run `finch %s start` to start the existing instance", + limaInstanceName, virtualMachineRootCmd) + case lima.Running: + return fmt.Errorf("the instance %q is already running", limaInstanceName) + default: + return nil + } +} diff --git a/cmd/virtual_machine_init_test.go b/cmd/virtual_machine_init_test.go new file mode 100644 index 000000000..2e3b55641 --- /dev/null +++ b/cmd/virtual_machine_init_test.go @@ -0,0 +1,298 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +const mockBaseYamlFilePath = "/os/os.yaml" + +func TestNewInitVMCommand(t *testing.T) { + t.Parallel() + + cmd := newInitVMCommand(nil, nil, nil, nil, nil, "", nil, "") + assert.Equal(t, cmd.Name(), "init") +} + +func TestInitVMAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + command *cobra.Command + args []string + groups func(*gomock.Controller) []*dependency.Group + mockSvc func( + *mocks.LimaCmdCreator, + *mocks.Logger, + *mocks.LimaConfigApplier, + *gomock.Controller, + ) + }{ + { + name: "should init instance with correct BaseYamlFilePath", + command: &cobra.Command{ + Use: "init", + }, + args: []string{}, + groups: func(ctrl *gomock.Controller) []*dependency.Group { + dep := mocks.NewDependency(ctrl) + deps := dependency.NewGroup([]dependency.Dependency{dep}, "", "") + groups := []*dependency.Group{deps} + + dep.EXPECT().Installed().Return(false) + dep.EXPECT().Install().Return(nil) + dep.EXPECT().RequiresRoot().Return(false) + + return groups + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + + command := mocks.NewCommand(ctrl) + lca.EXPECT().Apply().Return(nil) + lcc.EXPECT().CreateWithoutStdio("start", fmt.Sprintf("--name=%s", limaInstanceName), + mockBaseYamlFilePath, "--tty=false").Return(command) + command.EXPECT().CombinedOutput() + + logger.EXPECT().Info("Initializing and starting Finch virtual machine...") + logger.EXPECT().Info("Finch virtual machine started successfully") + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + lca := mocks.NewLimaConfigApplier(ctrl) + + groups := tc.groups(ctrl) + tc.mockSvc(lcc, logger, lca, ctrl) + + assert.NoError(t, newInitVMAction(lcc, logger, groups, lca, mockBaseYamlFilePath).runAdapter(tc.command, tc.args)) + }) + } +} + +func TestInitVMAction_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + wantErr error + groups func(*gomock.Controller) []*dependency.Group + mockSvc func( + *mocks.LimaCmdCreator, + *mocks.Logger, + *mocks.LimaConfigApplier, + *gomock.Controller, + ) + }{ + { + name: "should init instance with correct BaseYamlFilePath", + wantErr: nil, + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + + lca.EXPECT().Apply().Return(nil) + + command := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("start", fmt.Sprintf("--name=%s", limaInstanceName), + mockBaseYamlFilePath, "--tty=false").Return(command) + command.EXPECT().CombinedOutput() + + logger.EXPECT().Info("Initializing and starting Finch virtual machine...") + logger.EXPECT().Info("Finch virtual machine started successfully") + }, + }, + { + name: "running VM", + wantErr: fmt.Errorf("the instance %q is already running", limaInstanceName), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + }, + }, + { + name: "stopped VM", + wantErr: fmt.Errorf( + "the instance %q already exists but is stopped, run `finch %s start` to start the existing instance", + limaInstanceName, virtualMachineRootCmd), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + }, + }, + { + name: "unknown VM status", + wantErr: errors.New("unrecognized system status"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + wantErr: errors.New("get status error"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), errors.New("get status error")) + }, + }, + { + // TODO: split this test case up: + // should succeed even if some optional dependencies fail to be installed + // return an error if Lima config fails to be applied + name: "should print out error if InstallOptionalDeps fails and return error if LoadAndApplyLimaConfig fails", + wantErr: errors.New("load config fails"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + dep := mocks.NewDependency(ctrl) + deps := dependency.NewGroup([]dependency.Dependency{dep}, "", "mock_error_msg") + groups := []*dependency.Group{deps} + + dep.EXPECT().Installed().Return(false) + dep.EXPECT().RequiresRoot().Return(false) + dep.EXPECT().Install().Return(errors.New("dependency error occurs")) + + return groups + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + + lca.EXPECT().Apply().Return(errors.New("load config fails")) + logger.EXPECT().Error(fmt.Sprintf("Dependency error: failed to install dependencies: %v", + []error{fmt.Errorf("%s: %v", "mock_error_msg", []error{errors.New("dependency error occurs")})}, + )) + }, + }, + { + name: "should print error if instance fails to initialize", + wantErr: errors.New("failed to init instance"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + + lca.EXPECT().Apply().Return(nil) + + logs := []byte("stdout + stderr") + command := mocks.NewCommand(ctrl) + command.EXPECT().CombinedOutput().Return(logs, errors.New("failed to init instance")) + lcc.EXPECT().CreateWithoutStdio("start", fmt.Sprintf("--name=%s", limaInstanceName), + mockBaseYamlFilePath, "--tty=false").Return(command) + + logger.EXPECT().Info("Initializing and starting Finch virtual machine...") + logger.EXPECT().Errorf("Finch virtual machine failed to start, debug logs: %s", logs) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + lca := mocks.NewLimaConfigApplier(ctrl) + + groups := tc.groups(ctrl) + tc.mockSvc(lcc, logger, lca, ctrl) + + err := newInitVMAction(lcc, logger, groups, lca, mockBaseYamlFilePath).run() + assert.Equal(t, err, tc.wantErr) + }) + } +} diff --git a/cmd/virtual_machine_remove.go b/cmd/virtual_machine_remove.go new file mode 100644 index 000000000..37614917b --- /dev/null +++ b/cmd/virtual_machine_remove.go @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/lima" + + "github.com/runfinch/finch/pkg/flog" +) + +func newRemoveVMCommand(limaCmdCreator command.LimaCmdCreator, logger flog.Logger) *cobra.Command { + removeVMCommand := &cobra.Command{ + Use: "remove", + Short: "Remove the virtual machine instance", + RunE: newRemoveVMAction(limaCmdCreator, logger).runAdapter, + } + + return removeVMCommand +} + +type removeVMAction struct { + creator command.LimaCmdCreator + logger flog.Logger +} + +func newRemoveVMAction(creator command.LimaCmdCreator, logger flog.Logger) *removeVMAction { + return &removeVMAction{creator: creator, logger: logger} +} + +func (rva *removeVMAction) runAdapter(cmd *cobra.Command, args []string) error { + return rva.run() +} + +func (rva *removeVMAction) run() error { + err := rva.assertVMIsStopped(rva.creator, rva.logger) + if err != nil { + return err + } + + limaCmd := rva.creator.CreateWithoutStdio("remove", limaInstanceName) + rva.logger.Info("Removing existing Finch virtual machine...") + logs, err := limaCmd.CombinedOutput() + if err != nil { + rva.logger.Errorf("Finch virtual machine failed to remove, debug logs: %s", logs) + return err + } + rva.logger.Info("Finch virtual machine removed successfully") + return nil +} + +func (rva *removeVMAction) assertVMIsStopped(creator command.LimaCmdCreator, logger flog.Logger) error { + status, err := lima.GetVMStatus(creator, logger, limaInstanceName) + if err != nil { + return err + } + switch status { + case lima.Nonexistent: + return fmt.Errorf("the instance %q does not exist", limaInstanceName) + case lima.Running: + return fmt.Errorf("the instance %q is running, run `finch %s stop` to stop the instance first", + limaInstanceName, virtualMachineRootCmd) + default: + return nil + } +} diff --git a/cmd/virtual_machine_remove_test.go b/cmd/virtual_machine_remove_test.go new file mode 100644 index 000000000..0b117cdcd --- /dev/null +++ b/cmd/virtual_machine_remove_test.go @@ -0,0 +1,166 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestNewRemoveVMCommand(t *testing.T) { + t.Parallel() + + cmd := newRemoveVMCommand(nil, nil) + assert.Equal(t, cmd.Name(), "remove") +} + +func TestRemoveVMAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.LimaCmdCreator, *gomock.Controller) + cmd *cobra.Command + args []string + }{ + { + name: "should remove the instance", + cmd: &cobra.Command{ + Use: "remove", + }, + args: []string{}, + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + command := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("remove", limaInstanceName).Return(command) + command.EXPECT().CombinedOutput() + logger.EXPECT().Info(gomock.Any()).AnyTimes() + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + + tc.mockSvc(logger, lcc, ctrl) + assert.NoError(t, newRemoveVMAction(lcc, logger).runAdapter(tc.cmd, tc.args)) + }) + } +} + +func TestRemoveVMAction_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + wantErr error + mockSvc func(*mocks.Logger, *mocks.LimaCmdCreator, *gomock.Controller) + }{ + { + name: "should remove the instance", + wantErr: nil, + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + command := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("remove", limaInstanceName).Return(command) + command.EXPECT().CombinedOutput() + logger.EXPECT().Info("Removing existing Finch virtual machine...") + logger.EXPECT().Info("Finch virtual machine removed successfully") + }, + }, + { + name: "running VM", + wantErr: fmt.Errorf("the instance %q is running, run `finch %s stop` to stop the instance first", + limaInstanceName, virtualMachineRootCmd), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + }, + }, + { + name: "nonexistent VM", + wantErr: fmt.Errorf("the instance %q does not exist", limaInstanceName), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + { + name: "unknown VM status", + wantErr: errors.New("unrecognized system status"), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + wantErr: errors.New("get status error"), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), errors.New("get status error")) + }, + }, + { + name: "should print error if virtual machine failed to remove", + wantErr: errors.New("failed to remove instance"), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + logs := []byte("stdout + stderr") + command := mocks.NewCommand(ctrl) + command.EXPECT().CombinedOutput().Return(logs, errors.New("failed to remove instance")) + creator.EXPECT().CreateWithoutStdio("remove", limaInstanceName).Return(command) + logger.EXPECT().Info("Removing existing Finch virtual machine...") + logger.EXPECT().Errorf("Finch virtual machine failed to remove, debug logs: %s", logs) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + + tc.mockSvc(logger, lcc, ctrl) + err := newRemoveVMAction(lcc, logger).run() + assert.Equal(t, tc.wantErr, err) + }) + } +} diff --git a/cmd/virtual_machine_start.go b/cmd/virtual_machine_start.go new file mode 100644 index 000000000..de86d156f --- /dev/null +++ b/cmd/virtual_machine_start.go @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/config" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/lima" + + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newStartVMCommand( + lcc command.LimaCmdCreator, + logger flog.Logger, + optionalDepGroups []*dependency.Group, + lca config.LimaConfigApplier, + nca config.NerdctlConfigApplier, + fs afero.Fs, + privateKeyPath string, +) *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "Start the virtual machine", + RunE: newStartVMAction(lcc, logger, optionalDepGroups, lca).runAdapter, + PostRunE: newPostVMStartInitAction(logger, lcc, fs, privateKeyPath, nca).runAdapter, + } +} + +type startVMAction struct { + creator command.LimaCmdCreator + logger flog.Logger + optionalDepGroups []*dependency.Group + limaConfigApplier config.LimaConfigApplier +} + +func newStartVMAction( + creator command.LimaCmdCreator, + logger flog.Logger, + optionalDepGroups []*dependency.Group, + lca config.LimaConfigApplier, +) *startVMAction { + return &startVMAction{creator: creator, logger: logger, optionalDepGroups: optionalDepGroups, limaConfigApplier: lca} +} + +func (sva *startVMAction) runAdapter(cmd *cobra.Command, args []string) error { + return sva.run() +} + +func (sva *startVMAction) run() error { + err := sva.assertVMIsStopped(sva.creator, sva.logger) + if err != nil { + return err + } + err = dependency.InstallOptionalDeps(sva.optionalDepGroups, sva.logger) + if err != nil { + sva.logger.Error(fmt.Sprintf("Dependency error: %s", err)) + } + + err = sva.limaConfigApplier.Apply() + if err != nil { + return err + } + + limaCmd := sva.creator.CreateWithoutStdio("start", limaInstanceName) + sva.logger.Info("Starting existing Finch virtual machine...") + logs, err := limaCmd.CombinedOutput() + if err != nil { + sva.logger.Errorf("Finch virtual machine failed to start, debug logs: %s", logs) + return err + } + sva.logger.Info("Finch virtual machine started successfully") + return nil +} + +func (sva *startVMAction) assertVMIsStopped(creator command.LimaCmdCreator, logger flog.Logger) error { + status, err := lima.GetVMStatus(creator, logger, limaInstanceName) + if err != nil { + return err + } + switch status { + case lima.Nonexistent: + return fmt.Errorf("the instance %q does not exist, run `finch %s init` to create a new instance", + limaInstanceName, virtualMachineRootCmd) + case lima.Running: + return fmt.Errorf("the instance %q is already running", limaInstanceName) + default: + return nil + } +} diff --git a/cmd/virtual_machine_start_test.go b/cmd/virtual_machine_start_test.go new file mode 100644 index 000000000..d8b155801 --- /dev/null +++ b/cmd/virtual_machine_start_test.go @@ -0,0 +1,311 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestNewStartVMCommand(t *testing.T) { + t.Parallel() + + cmd := newStartVMCommand(nil, nil, nil, nil, nil, nil, "") + assert.Equal(t, cmd.Name(), "start") +} + +func TestStartVMAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + wantErr error + command *cobra.Command + args []string + groups func(*gomock.Controller) []*dependency.Group + mockSvc func( + *mocks.LimaCmdCreator, + *mocks.Logger, + *mocks.LimaConfigApplier, + *gomock.Controller, + ) + }{ + { + name: "should start instance", + wantErr: nil, + command: &cobra.Command{ + Use: "start", + }, + groups: func(ctrl *gomock.Controller) []*dependency.Group { + dep := mocks.NewDependency(ctrl) + deps := dependency.NewGroup([]dependency.Dependency{dep}, "", "") + groups := []*dependency.Group{deps} + + dep.EXPECT().Installed().Return(false) + dep.EXPECT().RequiresRoot().Return(false) + dep.EXPECT().Install().Return(nil) + + return groups + }, + args: []string{}, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + lca.EXPECT().Apply().Return(nil) + + command := mocks.NewCommand(ctrl) + command.EXPECT().CombinedOutput() + lcc.EXPECT().CreateWithoutStdio("start", limaInstanceName).Return(command) + + logger.EXPECT().Info("Starting existing Finch virtual machine...") + logger.EXPECT().Info("Finch virtual machine started successfully") + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + lca := mocks.NewLimaConfigApplier(ctrl) + + groups := tc.groups(ctrl) + tc.mockSvc(lcc, logger, lca, ctrl) + + err := newStartVMAction(lcc, logger, groups, lca).runAdapter(tc.command, tc.args) + assert.Equal(t, tc.wantErr, err) + }) + } +} + +func TestStartVMAction_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + wantErr error + groups func(*gomock.Controller) []*dependency.Group + mockSvc func( + *mocks.LimaCmdCreator, + *mocks.Logger, + *mocks.LimaConfigApplier, + *gomock.Controller, + ) + }{ + { + name: "should start instance if instance exists", + wantErr: nil, + groups: func(ctrl *gomock.Controller) []*dependency.Group { + dep := mocks.NewDependency(ctrl) + deps := dependency.NewGroup([]dependency.Dependency{dep}, "", "") + groups := []*dependency.Group{deps} + + dep.EXPECT().Installed().Return(false) + dep.EXPECT().RequiresRoot().Return(false) + dep.EXPECT().Install().Return(nil) + + return groups + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + lca.EXPECT().Apply().Return(nil) + + command := mocks.NewCommand(ctrl) + command.EXPECT().CombinedOutput() + lcc.EXPECT().CreateWithoutStdio("start", limaInstanceName).Return(command) + + logger.EXPECT().Info("Starting existing Finch virtual machine...") + logger.EXPECT().Info("Finch virtual machine started successfully") + }, + }, + { + name: "running VM", + wantErr: fmt.Errorf("the instance %q is already running", limaInstanceName), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + }, + }, + { + name: "nonexistent VM", + wantErr: fmt.Errorf("the instance %q does not exist, run `finch %s init` to create a new instance", + limaInstanceName, virtualMachineRootCmd), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + { + name: "unknown VM status", + wantErr: errors.New("unrecognized system status"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + wantErr: errors.New("get status error"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + return nil + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), errors.New("get status error")) + }, + }, + { + // TODO: split this test case up: + // should succeed even if some optional dependencies fail to be installed + // return an error if Lima config fails to be applied + name: "should print out error if InstallOptionalDeps fails and return error if LoadAndApplyLimaConfig fails", + wantErr: errors.New("load config fails"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + dep := mocks.NewDependency(ctrl) + deps := dependency.NewGroup([]dependency.Dependency{dep}, "", "mock_error_msg") + groups := []*dependency.Group{deps} + + dep.EXPECT().Installed().Return(false) + dep.EXPECT().RequiresRoot().Return(false) + dep.EXPECT().Install().Return(errors.New("dependency error occurs")) + + return groups + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + lca.EXPECT().Apply().Return(errors.New("load config fails")) + + logger.EXPECT().Error(fmt.Sprintf("Dependency error: failed to install dependencies: %v", + []error{fmt.Errorf("%s: %v", "mock_error_msg", []error{errors.New("dependency error occurs")})}, + )) + }, + }, + { + name: "should print out error if instance fails to start", + wantErr: errors.New("start command error"), + groups: func(ctrl *gomock.Controller) []*dependency.Group { + dep := mocks.NewDependency(ctrl) + deps := dependency.NewGroup([]dependency.Dependency{dep}, "", "") + groups := []*dependency.Group{deps} + + dep.EXPECT().Installed().Return(true) + + return groups + }, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + + lca.EXPECT().Apply().Return(nil) + + logs := []byte("stdout + stderr") + command := mocks.NewCommand(ctrl) + command.EXPECT().CombinedOutput().Return(logs, errors.New("start command error")) + lcc.EXPECT().CreateWithoutStdio("start", limaInstanceName).Return(command) + + logger.EXPECT().Info("Starting existing Finch virtual machine...") + logger.EXPECT().Errorf("Finch virtual machine failed to start, debug logs: %s", logs) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + lca := mocks.NewLimaConfigApplier(ctrl) + + groups := tc.groups(ctrl) + tc.mockSvc(lcc, logger, lca, ctrl) + + err := newStartVMAction(lcc, logger, groups, lca).run() + assert.Equal(t, err, tc.wantErr) + }) + } +} diff --git a/cmd/virtual_machine_stop.go b/cmd/virtual_machine_stop.go new file mode 100644 index 000000000..cabe01583 --- /dev/null +++ b/cmd/virtual_machine_stop.go @@ -0,0 +1,69 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/lima" + + "github.com/spf13/cobra" +) + +func newStopVMCommand(limaCmdCreator command.LimaCmdCreator, logger flog.Logger) *cobra.Command { + stopVMCommand := &cobra.Command{ + Use: "stop", + Short: "Stop the virtual machine", + RunE: newStopVMAction(limaCmdCreator, logger).runAdapter, + } + + return stopVMCommand +} + +type stopVMAction struct { + creator command.LimaCmdCreator + logger flog.Logger +} + +func newStopVMAction(creator command.LimaCmdCreator, logger flog.Logger) *stopVMAction { + return &stopVMAction{creator: creator, logger: logger} +} + +func (sva *stopVMAction) runAdapter(cmd *cobra.Command, args []string) error { + return sva.run() +} + +func (sva *stopVMAction) run() error { + err := sva.assertVMIsRunning(sva.creator, sva.logger) + if err != nil { + return err + } + + limaCmd := sva.creator.CreateWithoutStdio("stop", limaInstanceName) + sva.logger.Info("Stopping existing Finch virtual machine...") + logs, err := limaCmd.CombinedOutput() + if err != nil { + sva.logger.Errorf("Finch virtual machine failed to stop, debug logs: %s", logs) + return err + } + sva.logger.Info("Finch virtual machine stopped successfully") + return nil +} + +func (sva *stopVMAction) assertVMIsRunning(creator command.LimaCmdCreator, logger flog.Logger) error { + status, err := lima.GetVMStatus(creator, logger, limaInstanceName) + if err != nil { + return err + } + switch status { + case lima.Nonexistent: + return fmt.Errorf("the instance %q does not exist", limaInstanceName) + case lima.Stopped: + return fmt.Errorf("the instance %q is already stopped", limaInstanceName) + default: + return nil + } +} diff --git a/cmd/virtual_machine_stop_test.go b/cmd/virtual_machine_stop_test.go new file mode 100644 index 000000000..2966843dd --- /dev/null +++ b/cmd/virtual_machine_stop_test.go @@ -0,0 +1,165 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestNewStopVMCommand(t *testing.T) { + t.Parallel() + + cmd := newStopVMCommand(nil, nil) + assert.Equal(t, cmd.Name(), "stop") +} + +func TestStopVMAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.LimaCmdCreator, *gomock.Controller) + cmd *cobra.Command + args []string + }{ + { + name: "should stop the instance", + cmd: &cobra.Command{ + Use: "stop", + }, + args: []string{}, + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + + command := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("stop", limaInstanceName).Return(command) + command.EXPECT().CombinedOutput() + logger.EXPECT().Info(gomock.Any()).AnyTimes() + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + + tc.mockSvc(logger, lcc, ctrl) + assert.NoError(t, newStopVMAction(lcc, logger).runAdapter(tc.cmd, tc.args)) + }) + } +} + +func TestStopVMAction_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + wantErr error + mockSvc func(*mocks.Logger, *mocks.LimaCmdCreator, *gomock.Controller) + }{ + { + name: "should stop the instance", + wantErr: nil, + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + + command := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("stop", limaInstanceName).Return(command) + command.EXPECT().CombinedOutput() + logger.EXPECT().Info("Stopping existing Finch virtual machine...") + logger.EXPECT().Info("Finch virtual machine stopped successfully") + }, + }, + { + name: "stopped VM", + wantErr: fmt.Errorf("the instance %q is already stopped", limaInstanceName), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + }, + }, + { + name: "nonexistent VM", + wantErr: fmt.Errorf("the instance %q does not exist", limaInstanceName), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + { + name: "unknown VM status", + wantErr: errors.New("unrecognized system status"), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + wantErr: errors.New("get status error"), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), errors.New("get status error")) + }, + }, + { + name: "should print error if virtual machine failed to stop", + wantErr: errors.New("error"), + mockSvc: func(logger *mocks.Logger, creator *mocks.LimaCmdCreator, ctrl *gomock.Controller) { + getVMStatusC := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + + logs := []byte("stdout + stderr") + command := mocks.NewCommand(ctrl) + command.EXPECT().CombinedOutput().Return(logs, errors.New("error")) + creator.EXPECT().CreateWithoutStdio("stop", limaInstanceName).Return(command) + logger.EXPECT().Info("Stopping existing Finch virtual machine...") + logger.EXPECT().Errorf("Finch virtual machine failed to stop, debug logs: %s", logs) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + + tc.mockSvc(logger, lcc, ctrl) + err := newStopVMAction(lcc, logger).run() + assert.Equal(t, tc.wantErr, err) + }) + } +} diff --git a/cmd/virtual_machine_test.go b/cmd/virtual_machine_test.go new file mode 100644 index 000000000..0a8d9f38d --- /dev/null +++ b/cmd/virtual_machine_test.go @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestVirtualMachineCommand(t *testing.T) { + t.Parallel() + + cmd := newVirtualMachineCommand(nil, nil, nil, nil, nil, "", nil) + assert.Equal(t, cmd.Use, virtualMachineRootCmd) + + // check the number of subcommand for vm + assert.Equal(t, len(cmd.Commands()), 4) +} + +func TestPostVMStartInitAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.LimaCmdCreator, *mocks.Command, *mocks.NerdctlConfigApplier) + cmd *cobra.Command + args []string + wantErr error + }{ + { + name: "config files are applied after boot", + mockSvc: func(logger *mocks.Logger, lcc *mocks.LimaCmdCreator, command *mocks.Command, nca *mocks.NerdctlConfigApplier) { + logger.EXPECT().Debugln("Applying guest configuration options") + command.EXPECT().Output().Return([]byte("80"), nil) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.SSHLocalPort}}", limaInstanceName).Return(command) + nca.EXPECT().Apply("127.0.0.1:80").Return(nil) + }, + cmd: &cobra.Command{ + Use: "init", + }, + args: []string{}, + wantErr: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + command := mocks.NewCommand(ctrl) + nca := mocks.NewNerdctlConfigApplier(ctrl) + tc.mockSvc(logger, lcc, command, nca) + + err := newPostVMStartInitAction(logger, lcc, nil, "", nca).runAdapter(tc.cmd, tc.args) + assert.Equal(t, err, tc.wantErr) + }) + } +} + +func TestPostVMStartInitAction_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.LimaCmdCreator, *mocks.Command, *mocks.NerdctlConfigApplier) + wantErr error + }{ + { + name: "config files are applied after boot", + mockSvc: func(logger *mocks.Logger, lcc *mocks.LimaCmdCreator, command *mocks.Command, nca *mocks.NerdctlConfigApplier) { + logger.EXPECT().Debugln("Applying guest configuration options") + command.EXPECT().Output().Return([]byte("80"), nil) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.SSHLocalPort}}", limaInstanceName).Return(command) + nca.EXPECT().Apply("127.0.0.1:80").Return(nil) + }, + wantErr: nil, + }, + { + name: "should return an error if sshPortCmd has an error output", + mockSvc: func(logger *mocks.Logger, lcc *mocks.LimaCmdCreator, command *mocks.Command, nca *mocks.NerdctlConfigApplier) { + logger.EXPECT().Debugln("Applying guest configuration options") + command.EXPECT().Output().Return(nil, errors.New("ssh port error")) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.SSHLocalPort}}", limaInstanceName).Return(command) + }, + wantErr: errors.New("ssh port error"), + }, + { + name: "should print info and return without error if port is 0", + mockSvc: func(logger *mocks.Logger, lcc *mocks.LimaCmdCreator, command *mocks.Command, nca *mocks.NerdctlConfigApplier) { + logger.EXPECT().Debugln("Applying guest configuration options") + command.EXPECT().Output().Return([]byte("0"), nil) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.SSHLocalPort}}", limaInstanceName).Return(command) + logger.EXPECT().Warnln("SSH port = 0, is the instance running? Not able to apply VM configuration options") + }, + wantErr: nil, + }, + { + name: "should return error if applyNerdctlConfig has an error", + mockSvc: func(logger *mocks.Logger, lcc *mocks.LimaCmdCreator, command *mocks.Command, nca *mocks.NerdctlConfigApplier) { + logger.EXPECT().Debugln("Applying guest configuration options") + command.EXPECT().Output().Return([]byte("80"), nil) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.SSHLocalPort}}", limaInstanceName).Return(command) + nca.EXPECT().Apply("127.0.0.1:80").Return(errors.New("applyNerdctlConfig has an error")) + }, + wantErr: errors.New("applyNerdctlConfig has an error"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + lcc := mocks.NewLimaCmdCreator(ctrl) + command := mocks.NewCommand(ctrl) + nca := mocks.NewNerdctlConfigApplier(ctrl) + tc.mockSvc(logger, lcc, command, nca) + + err := newPostVMStartInitAction(logger, lcc, nil, "", nca).run() + assert.Equal(t, err, tc.wantErr) + }) + } +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 000000000..d5b00a556 --- /dev/null +++ b/config.yaml @@ -0,0 +1,3 @@ +# Every field is optional, even an empty file would work +memory: 4GiB +cpus: 4 diff --git a/copyright_header b/copyright_header new file mode 100644 index 000000000..478e61053 --- /dev/null +++ b/copyright_header @@ -0,0 +1,2 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/e2e/config_test.go b/e2e/config_test.go new file mode 100644 index 000000000..4fe029367 --- /dev/null +++ b/e2e/config_test.go @@ -0,0 +1,118 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" +) + +var finchConfigFilePath = os.Getenv("HOME") + "/.finch/finch.yaml" + +const limaConfigFilePath = "../_output/lima/data/_config/override.yaml" + +func readFile(filePath string) []byte { + out, err := os.ReadFile(filepath.Clean(filePath)) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + } + return out +} + +func writeFile(filePath string, buf []byte) { + gomega.Expect(os.WriteFile(filePath, buf, 0o644)).ShouldNot(gomega.HaveOccurred()) +} + +// updateAndApplyConfig writes to the config file, and stop/starts the VM to apply its values. +// It returns the session of the final start command so it can be used in further assertions. +func updateAndApplyConfig(o *option.Option, configBytes []byte) *gexec.Session { + writeFile(finchConfigFilePath, configBytes) + + command.New(o, virtualMachineRootCmd, "stop").WithoutCheckingExitCode().WithTimeoutInSeconds(20).Run() + return command.New(o, virtualMachineRootCmd, "start").WithoutCheckingExitCode().WithTimeoutInSeconds(60).Run() +} + +// testConfig updates the finch config file and ensures that its settings are applied properly. +// +// Many test cases only check partial file contents, rather than strictly checking +// the exact contents, because other modules can update the same files. +// +// For example, pkg/dependency/vde/update_override_lima_config.go's appendNetworkConfiguration function +// updates the lima override.yaml config file, independently of the config file module's function. +// +// For simplicity, the cleanup for this test suite currently does not distinguish between an +// empty and a non-existent Finch config.yaml. Meaning, if you run this without an existing config.yaml, +// an empty config.yaml will be created after all test cases are run. This currently does not change the behavior +// of Finch, but may need to be revisited later. +var testConfig = func(o *option.Option) { + // These tests are run in serial because we only define one virtual machine instance, and it requires disk I/O. + ginkgo.Describe("Config", ginkgo.Serial, func() { + ginkgo.BeforeEach(func() { + origFinchCfg := readFile(finchConfigFilePath) + origLimaCfg := readFile(limaConfigFilePath) + + ginkgo.DeferCleanup(func() { + writeFile(finchConfigFilePath, origFinchCfg) + writeFile(limaConfigFilePath, origLimaCfg) + + command.New(o, virtualMachineRootCmd, "stop").WithoutCheckingExitCode().WithTimeoutInSeconds(60).Run() + command.New(o, virtualMachineRootCmd, "start").WithTimeoutInSeconds(120).Run() + }) + }) + + ginkgo.It("updates config values when a config file is present", func() { + startCmdSession := updateAndApplyConfig(o, []byte("memory: 4GiB\ncpus: 6")) + gomega.Expect(startCmdSession).Should(gexec.Exit(0)) + + gomega.Expect(limaConfigFilePath).Should(gomega.BeARegularFile()) + cfgBuf, err := os.ReadFile(limaConfigFilePath) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(cfgBuf).Should(gomega.SatisfyAll(gomega.ContainSubstring("cpus: 6"), gomega.ContainSubstring("memory: 4GiB"))) + }) + + ginkgo.It("updates config values when partial config file is present", func() { + startCmdSession := updateAndApplyConfig(o, []byte("memory: 6GiB")) + gomega.Expect(startCmdSession).Should(gexec.Exit(0)) + + gomega.Expect(limaConfigFilePath).Should(gomega.BeARegularFile()) + cfgBuf, err := os.ReadFile(limaConfigFilePath) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + // 4 CPUs is the default + gomega.Expect(cfgBuf).Should(gomega.SatisfyAll(gomega.MatchRegexp(`cpus: \d`), gomega.ContainSubstring("memory: 6GiB"))) + }) + + ginkgo.It("uses the default config values when no config file is present", func() { + startCmdSession := updateAndApplyConfig(o, nil) + gomega.Expect(startCmdSession).Should(gexec.Exit(0)) + + gomega.Expect(limaConfigFilePath).Should(gomega.BeARegularFile()) + cfgBuf, err := os.ReadFile(limaConfigFilePath) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(cfgBuf).Should(gomega.SatisfyAll(gomega.MatchRegexp(`cpus: \d`), gomega.MatchRegexp(`memory: \dGiB`))) + }) + + ginkgo.It("fails to launch when the config file is improperly formatted", func() { + startCmdSession := updateAndApplyConfig(o, []byte("this isn't yaml")) + gomega.Expect(startCmdSession).Should(gexec.Exit(1)) + }) + + ginkgo.It("fails to launch when the config file file doesn't specify enough CPUs", func() { + startCmdSession := updateAndApplyConfig(o, []byte("cpus: 0")) + gomega.Expect(startCmdSession).Should(gexec.Exit(1)) + }) + + ginkgo.It("fails to launch when the config file doesn't specify enough memory", func() { + startCmdSession := updateAndApplyConfig(o, []byte("memory: 0GiB")) + gomega.Expect(startCmdSession).Should(gexec.Exit(1)) + }) + }) +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 000000000..078eb85f2 --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "os" + "path/filepath" + "testing" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" + "github.com/runfinch/common-tests/tests" +) + +const virtualMachineRootCmd = "vm" + +//nolint:paralleltest // TestE2e is like TestMain for our e2e tests. +func TestE2e(t *testing.T) { + description := "Finch CLI e2e Tests" + + wd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get the current working directory: %v", err) + } + subject := filepath.Join(wd, "../_output/bin/finch") + + o, err := option.New([]string{subject}) + if err != nil { + t.Fatalf("failed to initialize a testing option: %v", err) + } + + ginkgo.SynchronizedBeforeSuite(func() []byte { + command.New(o, "vm", "init").WithTimeoutInSeconds(600).Run() + tests.SetupLocalRegistry(o) + return nil + }, func(bytes []byte) {}) + + ginkgo.SynchronizedAfterSuite(func() { + command.New(o, "vm", "stop").WithTimeoutInSeconds(60).Run() + command.New(o, "vm", "remove").WithTimeoutInSeconds(60).Run() + }, func() {}) + + ginkgo.Describe(description, func() { + tests.Pull(o) + tests.Rm(o) + tests.Rmi(o) + tests.Run(o) + tests.Start(o) + tests.Stop(o) + tests.Tag(o) + tests.Save(o) + tests.Load(o) + tests.Build(o) + tests.Push(o) + tests.Images(o) + tests.ComposeBuild(o) + tests.ComposeDown(o) + tests.ComposeKill(o) + tests.ComposePs(o) + tests.ComposePull(o) + tests.ComposeLogs(o) + tests.Create(o) + tests.Port(o) + tests.Kill(o) + tests.Stats(o) + tests.BuilderPrune(o) + tests.Exec(o) + tests.Logs(o) + tests.Login(o) + tests.Logout(o) + tests.VolumeCreate(o) + tests.VolumeInspect(o) + tests.VolumeLs(o) + tests.VolumeRm(o) + tests.VolumePrune(o) + tests.ImageHistory(o) + tests.ImageInspect(o) + tests.ImagePrune(o) + tests.Info(o) + tests.Events(o) + tests.Inspect(o) + tests.NetworkCreate(o) + tests.NetworkInspect(o) + tests.NetworkLs(o) + tests.NetworkRm(o) + // When running tests in serial sequence and using the local registry, testVirtualMachine needs to run after generic tests finished + // since it will remove the VM instance thus removing the local registry. + testVirtualMachine(o) + testConfig(o) + testVersion(o) + }) + + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, description) +} diff --git a/e2e/version_test.go b/e2e/version_test.go new file mode 100644 index 000000000..332fdc7cd --- /dev/null +++ b/e2e/version_test.go @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "fmt" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" +) + +var testVersion = func(o *option.Option) { + ginkgo.Context("Version", func() { + ginkgo.Specify("Test version", func() { + gomega.Expect(command.StdoutStr(o, "version")).To(gomega.Equal(fmt.Sprintf("Finch version: %s", "v0.1.0"))) + }) + }) +} diff --git a/e2e/virtual_machine_test.go b/e2e/virtual_machine_test.go new file mode 100644 index 000000000..efb02bf82 --- /dev/null +++ b/e2e/virtual_machine_test.go @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "github.com/onsi/ginkgo/v2" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" +) + +var testVirtualMachine = func(o *option.Option) { + // These tests are run in serial because we only define one virtual machine instance. + ginkgo.Describe("virtual machine lifecycle", ginkgo.Serial, func() { + ginkgo.When("the virtual machine is in running status", func() { + ginkgo.It("should fail to init/remove the virtual machine", func() { + command.New(o, virtualMachineRootCmd, "init").WithoutSuccessfulExit().Run() + command.New(o, virtualMachineRootCmd, "remove").WithoutSuccessfulExit().Run() + }) + + ginkgo.It("should be able to stop the virtual machine", func() { + command.Run(o, "images") + command.New(o, virtualMachineRootCmd, "stop").WithTimeoutInSeconds(60).Run() + command.RunWithoutSuccessfulExit(o, "images") + }) + }) + + ginkgo.When("the virtual machine is in stopped status", func() { + ginkgo.It("should fail to init/stop", func() { + command.New(o, virtualMachineRootCmd, "stop").WithoutSuccessfulExit().Run() + command.New(o, virtualMachineRootCmd, "init").WithoutSuccessfulExit().Run() + }) + + ginkgo.It("should be able to start the virtual machine", func() { + command.New(o, virtualMachineRootCmd, "start").WithTimeoutInSeconds(120).Run() + command.Run(o, "images") + }) + + ginkgo.It("should be able to remove the virtual machine", func() { + command.New(o, virtualMachineRootCmd, "stop").WithTimeoutInSeconds(60).Run() + command.New(o, virtualMachineRootCmd, "remove").WithTimeoutInSeconds(60).Run() + command.RunWithoutSuccessfulExit(o, "images") + }) + }) + + ginkgo.When("the virtual machine instance does not exist", func() { + ginkgo.It("should fail to start/stop", func() { + command.New(o, virtualMachineRootCmd, "start").WithoutSuccessfulExit().Run() + command.New(o, virtualMachineRootCmd, "stop").WithoutSuccessfulExit().Run() + }) + + ginkgo.It("should be able to init", func() { + command.New(o, virtualMachineRootCmd, "init").WithTimeoutInSeconds(600).Run() + command.Run(o, "images") + }) + }) + }) +} diff --git a/finch.yaml b/finch.yaml new file mode 100644 index 000000000..55f904f3d --- /dev/null +++ b/finch.yaml @@ -0,0 +1,187 @@ +# ===================================================================== # +# BASIC CONFIGURATION +# ===================================================================== # + +# Default values in this YAML file are specified by `null` instead of Lima's "builtin default" values, +# so they can be overridden by the $LIMA_HOME/_config/default.yaml mechanism documented at the end of this file. + +# Arch: "default", "x86_64", "aarch64". +# 🟢 Builtin default: "default" (corresponds to the host architecture) +arch: null + +# OpenStack-compatible disk image. +# 🟢 Builtin default: null (must be specified) +# 🔵 This file: Ubuntu 22.04 Jammy Jellyfish images +images: + - location: "" + arch: "" + digest: "" + +# CPUs: if you see performance issues, try limiting cpus to 1. +# 🟢 Builtin default: 4 +cpus: null + +# Memory size +# 🟢 Builtin default: "4GiB" +memory: "4GiB" + +# Disk size +# 🟢 Builtin default: "100GiB" +disk: null + +# Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest +# 🟢 Builtin default: null (Mount nothing) +# 🔵 This file: Mount the home as read-only, /tmp/lima as writable +mounts: +- location: "~" + # Configure the mountPoint inside the guest. + # 🟢 Builtin default: value of location + mountPoint: null + # CAUTION: `writable` SHOULD be false for the home directory. + # Setting `writable` to true is safe for finch use case. + # Reference: https://github.com/lima-vm/lima/discussions/550 + writable: true + sshfs: + # Enabling the SSHFS cache will increase performance of the mounted filesystem, at + # the cost of potentially not reflecting changes made on the host in a timely manner. + # Warning: It looks like PHP filesystem access does not work correctly when + # the cache is disabled. + # 🟢 Builtin default: true + cache: null + # SSHFS has an optional flag called 'follow_symlinks'. This allows mounts + # to be properly resolved in the guest os and allow for access to the + # contents of the symlink. As a result, symlinked files & folders on the Host + # system will look and feel like regular files directories in the Guest OS. + # 🟢 Builtin default: false + followSymlinks: null + # SFTP driver, "builtin" or "openssh-sftp-server". "openssh-sftp-server" is recommended. + # 🟢 Builtin default: "openssh-sftp-server" if OpenSSH SFTP Server binary is found, otherwise "builtin" + sftpDriver: null + 9p: + # Supported security models are "passthrough", "mapped-xattr", "mapped-file" and "none". + # 🟢 Builtin default: "mapped-xattr" + securityModel: null + # Select 9P protocol version. Valid options are: "9p2000" (legacy), "9p2000.u", "9p2000.L". + # 🟢 Builtin default: "9p2000.L" + protocolVersion: null + # The number of bytes to use for 9p packet payload, where 4KiB is the absolute minimum. + # 🟢 Builtin default: "128KiB" + msize: null + # Specifies a caching policy. Valid options are: "none", "loose", "fscache" and "mmap". + # Try choosing "mmap" or "none" if you see a stability issue with the default "fscache". + # See https://www.kernel.org/doc/Documentation/filesystems/9p.txt + # 🟢 Builtin default: "fscache" for non-writable mounts, "mmap" for writable mounts + cache: null +- location: "/tmp/lima" + # 🟢 Builtin default: false + # 🔵 This file: true (only for "/tmp/lima") + writable: true + +# Mount type for above mounts, such as "reverse-sshfs" (from sshocker) or "9p" (EXPERIMENTAL, from QEMU’s virtio-9p-pci, aka virtfs) +# 🟢 Builtin default: "reverse-sshfs" +mountType: reverse-sshfs + +ssh: + # A localhost port of the host. Forwarded to port 22 of the guest. + # 🟢 Builtin default: 0 (automatically assigned to a free port) + # NOTE: when the instance name is "default", the builtin default value is set to + # 60022 for backward compatibility. + localPort: 0 + # Load ~/.ssh/*.pub in addition to $LIMA_HOME/_config/user.pub . + # This option is useful when you want to use other SSH-based + # applications such as rsync with the Lima instance. + # If you have an insecure key under ~/.ssh, do not use this option. + # 🟢 Builtin default: true + loadDotSSHPubKeys: null + # Forward ssh agent into the instance. + # 🟢 Builtin default: false + forwardAgent: null + # Forward X11 into the instance + # 🟢 Builtin default: false + forwardX11: null + # Trust forwarded X11 clients + # 🟢 Builtin default: false + forwardX11Trusted: null + +# ===================================================================== # +# ADVANCED CONFIGURATION +# ===================================================================== # + +containerd: + # Enable system-wide (aka rootful) containerd and its dependencies (BuildKit, Stargz Snapshotter) + # 🟢 Builtin default: false + system: null + # Enable user-scoped (aka rootless) containerd and its dependencies + # 🟢 Builtin default: true + user: null +# # Override containerd archive +# # 🟢 Builtin default: hard-coded URL with hard-coded digest (see the output of `limactl info | jq .defaultTemplate.containerd.archives`) +# archives: +# - location: "~/Downloads/nerdctl-full-X.Y.Z-linux-amd64.tar.gz" +# arch: "x86_64" +# digest: "sha256:..." + +# Provisioning scripts need to be idempotent because they might be called +# multiple times, e.g. when the host VM is being restarted. +# 🟢 Builtin default: null +provision: +# Install packages needed for QEMU user-mode emulation +- mode: system + script: | + #!/bin/bash + dnf install -y --setopt=install_weak_deps=False qemu-user-static-aarch64 qemu-user-static-arm qemu-user-static-x86 +- mode: boot + script: | + systemctl stop NetworkManager-wait-online.service + systemctl reset-failed NetworkManager-wait-online.service + systemctl mask NetworkManager-wait-online.service +# # `user` is executed without the root privilege +# - mode: user +# script: | +# #!/bin/bash +# set -eux -o pipefail +# cat < ~/.vimrc +# set number +# EOF + +# Probe scripts to check readiness. +# 🟢 Builtin default: null +# probes: +# # Only `readiness` probes are supported right now. +# - mode: readiness +# description: vim to be installed +# script: | +# #!/bin/bash +# set -eux -o pipefail +# if ! timeout 30s bash -c "until command -v vim; do sleep 3; done"; then +# echo >&2 "vim is not installed yet" +# exit 1 +# fi +# hint: | +# vim was not installed in the guest. Make sure the package system is working correctly. +# Also see "/var/log/cloud-init-output.log" in the guest. + +# ===================================================================== # +# FURTHER ADVANCED CONFIGURATION +# ===================================================================== # + +# Specify desired QEMU CPU type for each arch. +# You can see what options are available for host emulation with: `qemu-system-$(arch) -cpu help`. +# Setting of instructions is supported like this: "qemu64,+ssse3". +cpuType: + # 🟢 Builtin default: "cortex-a72" (or "host" when running on aarch64 host) + aarch64: null + # 🟢 Builtin default: "qemu64" (or "host" when running on x86_64 host) + x86_64: null + +firmware: + # Use legacy BIOS instead of UEFI. Ignored for aarch64. + # 🟢 Builtin default: false + legacyBIOS: null + +video: + # QEMU display, e.g., "none", "cocoa", "sdl", "gtk". + # As of QEMU v6.2, enabling this is known to have negative impact + # on performance on macOS hosts: https://gitlab.com/qemu-project/qemu/-/issues/334 + # 🟢 Builtin default: "none" + display: null diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..13d024a1a --- /dev/null +++ b/go.mod @@ -0,0 +1,61 @@ +module github.com/runfinch/finch + +go 1.18 + +require ( + github.com/golang/mock v1.6.0 + github.com/google/go-licenses v1.5.0 + github.com/lima-vm/lima v0.12.0 + github.com/onsi/ginkgo/v2 v2.5.0 + github.com/onsi/gomega v1.24.1 + github.com/pelletier/go-toml v1.9.5 + github.com/pkg/sftp v1.13.5 + github.com/runfinch/common-tests v0.1.0 + github.com/sirupsen/logrus v1.9.0 + github.com/spf13/afero v1.9.2 + github.com/spf13/cobra v1.6.0 + github.com/stretchr/testify v1.8.0 + github.com/xorcare/pointer v1.2.1 + golang.org/x/crypto v0.1.0 + golang.org/x/tools v0.2.0 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.25.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/go-units v0.5.0 + github.com/emirpasic/gods v1.12.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/goccy/go-yaml v1.9.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/otiai10/copy v1.6.0 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/src-d/gcfg v1.4.0 // indirect + github.com/xanzy/ssh-agent v0.2.1 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/net v0.2.0 // indirect + golang.org/x/sys v0.2.0 // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect + gopkg.in/src-d/go-git.v4 v4.13.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + k8s.io/klog/v2 v2.80.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..ae25bcada --- /dev/null +++ b/go.sum @@ -0,0 +1,847 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.82.0/go.mod h1:vlKccHJGuFBFufnAnuB08dfEH9Y3H7dzDzRECFdC2TA= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.4.0/go.mod h1:cbaZxyScUhxl7ZAkNWiALgihfP75wS/fUsVNaa1r3vA= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0= +github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-licenses v1.5.0 h1:eR8anXI/2AjP+jiv/XkGYHQg/cs3Yo4h72vd8fNure0= +github.com/google/go-licenses v1.5.0/go.mod h1:8skt/LhA4SkfarXU8kU/jP/ZzLwMAd8ai50WXpU2cvU= +github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= +github.com/google/go-replayers/httpreplay v1.1.1/go.mod h1:gN9GeLIs7l6NUoVaSSnv2RiqK1NiwAmD0MrKeC9IIks= +github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 h1:TJsAqW6zLRMDTyGmc9TPosfn9OyVlHs8Hrn3pY6ONSY= +github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148/go.mod h1:rq9F0RSpNKlrefnf6ZYMHKUnEJBCNzf6AcCXMYBeYvE= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lima-vm/lima v0.12.0 h1:HedFwaQH7gLSia+JFZFUV0vdfjOVEU7qPHWOfcRaNj4= +github.com/lima-vm/lima v0.12.0/go.mod h1:RPuS4RuIasryy8HWdxHMDeq1PnnpzIvW1IRpiCdCzWI= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +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/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ= +github.com/otiai10/copy v1.6.0/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E= +github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= +github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/runfinch/common-tests v0.1.0 h1:KLNlu22YOwA7B6EyhDgAt1e48gidliuQipId3X+z8ww= +github.com/runfinch/common-tests v0.1.0/go.mod h1:UT+TUdxKAujzgh9u+PjQI4I8G+7Yhxaep0LEDez5MIg= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +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.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +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/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xorcare/pointer v1.2.1 h1:iJKGuxJcv37Ikp3DE/w9y5qYUMlRL+jJS5GekvbVUVc= +github.com/xorcare/pointer v1.2.1/go.mod h1:azsKh7oVwYB7C1o8P284fG8MvtErX/F5/dqXiaj71ak= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +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= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.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.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= +k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/networks.yaml b/networks.yaml new file mode 100644 index 000000000..9d412d486 --- /dev/null +++ b/networks.yaml @@ -0,0 +1,29 @@ +# Paths to vde executables. Because vde_vmnet is invoked via sudo it should be +# installed where only root can modify/replace it. This means also none of the +# parent directories should be writable by the user. +# +# The varRun directory also must not be writable by the user because it will +# include the vde_vmnet pid files. Those will be terminated via sudo, so replacing +# the pid files would allow killing of arbitrary privileged processes. varRun +# however MUST be writable by the daemon user. +# +# None of the paths segments may be symlinks, why it has to be /private/var +# instead of /var etc. +paths: + socketVMNet: /opt/finch/bin/socket_vmnet + varRun: /private/var/run/finch-lima + sudoers: /private/etc/sudoers.d/finch-lima + +group: everyone + +networks: + finch-shared: + mode: shared + gateway: 192.168.105.1 + dhcpEnd: 192.168.105.254 + netmask: 255.255.255.0 + host: + mode: host + gateway: 192.168.106.1 + dhcpEnd: 192.168.106.254 + netmask: 255.255.255.0 diff --git a/pkg/command/command.go b/pkg/command/command.go new file mode 100644 index 000000000..d726d585c --- /dev/null +++ b/pkg/command/command.go @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package command invokes external commands. +package command + +import "io" + +// Creator creates a Command. The semantics of the parameters are the same as those of exec.Command. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/command_command_creator.go -package=mocks -mock_names Creator=CommandCreator . Creator +type Creator interface { + Create(name string, args ...string) Command +} + +// Command contains 2 sets of methods. +// The first set contains the methods to configure the command to be run (e.g., SetEnv). +// The second set contains the methods to run the command (e.g., Output). +// The semantics of the methods conform to that of the counterpart of exec.Cmd. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/command_command.go -package=mocks -mock_names Command=Command . Command +type Command interface { + SetEnv([]string) + SetStdin(io.Reader) + SetStdout(io.Writer) + SetStderr(io.Writer) + + Run() error + Output() ([]byte, error) + CombinedOutput() ([]byte, error) +} diff --git a/pkg/command/exec.go b/pkg/command/exec.go new file mode 100644 index 000000000..f09f5deb9 --- /dev/null +++ b/pkg/command/exec.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "io" + "os/exec" +) + +// ExecCmdCreator implements CommandCreator by invoking functions offered by os/exec. +type ExecCmdCreator struct{} + +var _ Creator = (*ExecCmdCreator)(nil) + +// NewExecCmdCreator creates a new ExecCmdCreator. +func NewExecCmdCreator() *ExecCmdCreator { + return &ExecCmdCreator{} +} + +// Create creates a new Command. +func (ecc *ExecCmdCreator) Create(name string, args ...string) Command { + return newExecCmd(name, args...) +} + +func newExecCmd(name string, args ...string) *execCmd { + return &execCmd{ + exec.Command(name, args...), + } +} + +type execCmd struct { + *exec.Cmd +} + +var _ Command = (*execCmd)(nil) + +func (c *execCmd) Output() ([]byte, error) { + b, err := c.Cmd.Output() + return b, wrapIfExitError(err) +} + +func (c *execCmd) SetEnv(env []string) { + c.Env = env +} + +func (c *execCmd) SetStdin(stdin io.Reader) { + c.Stdin = stdin +} + +func (c *execCmd) SetStdout(stdout io.Writer) { + c.Stdout = stdout +} + +func (c *execCmd) SetStderr(stderr io.Writer) { + c.Stderr = stderr +} diff --git a/pkg/command/exec_test.go b/pkg/command/exec_test.go new file mode 100644 index 000000000..ff709380a --- /dev/null +++ b/pkg/command/exec_test.go @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +var mockEnv = []string{"env=1"} + +func TestExecCommandCreator_Create(t *testing.T) { + t.Parallel() + + got := NewExecCmdCreator().Create("") + assert.IsType(t, got, &execCmd{}) +} + +func TestExecCommand_SetEnv(t *testing.T) { + t.Parallel() + + cmd := newExecCmd("") + cmd.SetEnv(mockEnv) + assert.Equal(t, cmd.Env, mockEnv) +} + +func TestExecCommand_SetStdin(t *testing.T) { + t.Parallel() + + cmd := newExecCmd("") + buf := bytes.NewBuffer([]byte("test")) + cmd.SetStdin(buf) + assert.Equal(t, cmd.Stdin, buf) +} + +func TestExecCommand_SetStdout(t *testing.T) { + t.Parallel() + + cmd := newExecCmd("") + buf := bytes.NewBuffer([]byte("test")) + cmd.SetStdout(buf) + assert.Equal(t, cmd.Stdout, buf) +} + +func TestExecCommand_SetStderr(t *testing.T) { + t.Parallel() + + cmd := newExecCmd("") + buf := bytes.NewBuffer([]byte("test")) + cmd.SetStderr(buf) + assert.Equal(t, cmd.Stderr, buf) +} diff --git a/pkg/command/exit_error.go b/pkg/command/exit_error.go new file mode 100644 index 000000000..4fc2f8043 --- /dev/null +++ b/pkg/command/exit_error.go @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "errors" + "fmt" + "os/exec" +) + +type exitError struct { + wrapped *exec.ExitError +} + +func newExitError(wrapped *exec.ExitError) *exitError { + return &exitError{wrapped: wrapped} +} + +// wrapIfExitError makes the returned error's Error() print stderr along with the exit code if err is of type *exec.ExitError. +// That's useful because *exec.ExitError by default only prints the exit code, while stderr is usually needed to help debug. +func wrapIfExitError(err error) error { + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) { + return err + } + return newExitError(exitErr) +} + +func (e *exitError) Error() string { + return fmt.Sprintf("%s, stderr: %s", e.wrapped.Error(), e.wrapped.Stderr) +} + +func (e *exitError) Unwrap() error { + return e.wrapped +} diff --git a/pkg/command/exit_error_test.go b/pkg/command/exit_error_test.go new file mode 100644 index 000000000..ce4c1e362 --- /dev/null +++ b/pkg/command/exit_error_test.go @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "errors" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWrapIfExitError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in error + want error + }{ + { + name: "the original error should be returned if it is not of type *exec.ExitError", + in: errors.New("I am not an *exec.ExitError"), + want: errors.New("I am not an *exec.ExitError"), + }, + { + name: "the error should be wrapped if it is of type *exec.ExitError", + in: &exec.ExitError{Stderr: []byte("stderr")}, + want: &exitError{wrapped: &exec.ExitError{Stderr: []byte("stderr")}}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tc.want, wrapIfExitError(tc.in)) + }) + } +} + +func TestExitError_Error(t *testing.T) { + t.Parallel() + + want := ", stderr: some error" + got := newExitError(&exec.ExitError{Stderr: []byte("some error")}).Error() + assert.Equal(t, got, want) +} + +func TestExitError_Unwrap(t *testing.T) { + t.Parallel() + + want := &exec.ExitError{Stderr: []byte("some error")} + got := newExitError(want).Unwrap() + assert.Equal(t, got, want) +} diff --git a/pkg/command/lima.go b/pkg/command/lima.go new file mode 100644 index 000000000..5819d05f9 --- /dev/null +++ b/pkg/command/lima.go @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "bytes" + "fmt" + "io" + + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/system" +) + +const ( + envKeyLimaHome = "LIMA_HOME" + envKeyPath = "PATH" +) + +// LimaCmdCreator creates a limactl command. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/command_lima_cmd_creator.go -package=mocks -mock_names LimaCmdCreator=LimaCmdCreator . LimaCmdCreator +type LimaCmdCreator interface { + // Create creates a new Lima command and connects the stdio of it to the stdio of the current process. + Create(args ...string) Command + // CreateWithoutStdio creates a new Lima command without connecting the stdio of it to the stdio of the current process. + // It is usually used when either Output() or CombinedOutput() instead of Run() needs to be invoked on the returned command. + CreateWithoutStdio(args ...string) Command + // RunWithReplacingStdout runs a new Lima command, + // connects the stdio of it to the stdio of the current process, + // and replaces all the strings in stdout according to rs. + // + // The replacements are executed sequentially. + // For example, after executing the first replacement, the resultant stdout will be executed against the second replacement, and so on. + // + // The reason of directly buffering the string to replace instead of having a customized replacing io.Writer is + // that the io.Writer without buffering may fail replacing because one source string can be split to multiple writes. + // Implementing an io.Writer with buffering will be more complicated than the current implementation. + RunWithReplacingStdout(rs []Replacement, args ...string) error +} + +// Replacement contains source string to be replaced by target string. +type Replacement struct { + Source, Target string +} + +type limaCmdCreator struct { + cmdCreator Creator + logger flog.Logger + systemDeps LimaCmdCreatorSystemDeps + limaHomePath string + limactlPath string + qemuBinPath string +} + +var _ LimaCmdCreator = (*limaCmdCreator)(nil) + +// LimaCmdCreatorSystemDeps contains the system dependencies for NewLimaCmdCreator. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/lima_cmd_creator_system_deps.go -package=mocks -mock_names LimaCmdCreatorSystemDeps=LimaCmdCreatorSystemDeps . LimaCmdCreatorSystemDeps +type LimaCmdCreatorSystemDeps interface { + system.EnvironGetter + system.StdinGetter + system.StdoutGetter + system.StderrGetter + system.EnvGetter +} + +// NewLimaCmdCreator returns a LimaCmdCreator that creates limactl commands based on the provided lima-related paths. +func NewLimaCmdCreator( + cmdCreator Creator, + logger flog.Logger, + limaHomePath, limactlPath string, qemuBinPath string, + systemDeps LimaCmdCreatorSystemDeps, +) LimaCmdCreator { + return &limaCmdCreator{ + cmdCreator: cmdCreator, + logger: logger, + limaHomePath: limaHomePath, + limactlPath: limactlPath, + qemuBinPath: qemuBinPath, + systemDeps: systemDeps, + } +} + +func (lcc *limaCmdCreator) Create(args ...string) Command { + return lcc.create(lcc.systemDeps.Stdin(), lcc.systemDeps.Stdout(), lcc.systemDeps.Stderr(), args...) +} + +func (lcc *limaCmdCreator) CreateWithoutStdio(args ...string) Command { + return lcc.create(nil, nil, nil, args...) +} + +func (lcc *limaCmdCreator) RunWithReplacingStdout(rs []Replacement, args ...string) error { + var buf bytes.Buffer + err := lcc.create(lcc.systemDeps.Stdin(), + &buf, + lcc.systemDeps.Stderr(), + args...).Run() + if err != nil { + // Note that at this point, buf may contain something that should be replaced and then written to stdout, + // but we decide it's fine to omit it and just return the error now because: + // - stderr should be enough for the user to debug and retry the command. + // - The control flow is simpler. + return err + } + _, err = lcc.systemDeps.Stdout().Write(lcc.replaceBytes(buf.Bytes(), rs)) + if err != nil { + return err + } + return nil +} + +func (lcc *limaCmdCreator) replaceBytes(s []byte, rs []Replacement) []byte { + for _, r := range rs { + s = bytes.ReplaceAll(s, []byte(r.Source), []byte(r.Target)) + } + return s +} + +func (lcc *limaCmdCreator) create(stdin io.Reader, stdout, stderr io.Writer, args ...string) Command { + lcc.logger.Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", args, envKeyLimaHome, lcc.limaHomePath) + cmd := lcc.cmdCreator.Create(lcc.limactlPath, args...) + limaHomeEnv := fmt.Sprintf("%s=%s", envKeyLimaHome, lcc.limaHomePath) + path := lcc.systemDeps.Env(envKeyPath) + path = fmt.Sprintf("%s:%s", lcc.qemuBinPath, path) + pathEnv := fmt.Sprintf("%s=%s", envKeyPath, path) + env := append(lcc.systemDeps.Environ(), limaHomeEnv, pathEnv) + cmd.SetEnv(env) + cmd.SetStdin(stdin) + cmd.SetStdout(stdout) + cmd.SetStderr(stderr) + return cmd +} diff --git a/pkg/command/lima_test.go b/pkg/command/lima_test.go new file mode 100644 index 000000000..cf8d76651 --- /dev/null +++ b/pkg/command/lima_test.go @@ -0,0 +1,317 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package command_test is not named as command to avoid circular dependency (command <-> mocks). +package command_test + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/mocks" +) + +const ( + mockLimaHomePath = "/lima/home" + mockLimactlPath = "/lima/bin/limactl" + envKeyLimaHome = "LIMA_HOME" + mockQemuBinPath = "/lima/bin" + mockSystemPath = "/usr/bin" + envKeyPath = "PATH" + finalPath = mockQemuBinPath + ":" + mockSystemPath +) + +var mockArgs = []string{"shell", "finch"} + +func TestLimaCmdCreator_Create(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.CommandCreator, *mocks.Command, *mocks.LimaCmdCreatorSystemDeps) + wantErr error + }{ + { + name: "happy path", + wantErr: nil, + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, cmd *mocks.Command, lcd *mocks.LimaCmdCreatorSystemDeps) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", mockArgs, envKeyLimaHome, mockLimaHomePath) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Stdin().Return(nil) + lcd.EXPECT().Stdout().Return(nil) + lcd.EXPECT().Stderr().Return(nil) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + cmd.EXPECT().SetStdout(nil) + cmd.EXPECT().SetStderr(nil) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmdCreator := mocks.NewCommandCreator(ctrl) + cmd := mocks.NewCommand(ctrl) + logger := mocks.NewLogger(ctrl) + lcd := mocks.NewLimaCmdCreatorSystemDeps(ctrl) + tc.mockSvc(logger, cmdCreator, cmd, lcd) + command.NewLimaCmdCreator(cmdCreator, logger, mockLimaHomePath, mockLimactlPath, mockQemuBinPath, lcd).Create(mockArgs...) + }) + } +} + +func TestLimaCmdCreator_CreateWithoutStdio(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.CommandCreator, *mocks.Command, *mocks.LimaCmdCreatorSystemDeps) + wantErr error + }{ + { + name: "happy path", + wantErr: nil, + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, cmd *mocks.Command, lcd *mocks.LimaCmdCreatorSystemDeps) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", mockArgs, envKeyLimaHome, mockLimaHomePath) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + cmd.EXPECT().SetStdout(nil) + cmd.EXPECT().SetStderr(nil) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmdCreator := mocks.NewCommandCreator(ctrl) + cmd := mocks.NewCommand(ctrl) + logger := mocks.NewLogger(ctrl) + lcd := mocks.NewLimaCmdCreatorSystemDeps(ctrl) + tc.mockSvc(logger, cmdCreator, cmd, lcd) + command.NewLimaCmdCreator(cmdCreator, logger, mockLimaHomePath, mockLimactlPath, mockQemuBinPath, lcd). + CreateWithoutStdio(mockArgs...) + }) + } +} + +func TestLimaCmdCreator_RunWithReplacingStdout(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Logger, *mocks.CommandCreator, *mocks.LimaCmdCreatorSystemDeps, *gomock.Controller, string, *os.File) + wantErr error + stdoutRs []command.Replacement + inOut string + outOut string + }{ + { + name: "happy path", + wantErr: nil, + stdoutRs: []command.Replacement{{Source: "s1", Target: "t1"}, {Source: "s3", Target: "t3"}, {Source: "s6", Target: "t6"}}, + inOut: "s1 s2 ,s3 /s4 s1.s5", + outOut: "t1 s2 ,t3 /s4 t1.s5", + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, + lcd *mocks.LimaCmdCreatorSystemDeps, ctrl *gomock.Controller, inOut string, f *os.File, + ) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", mockArgs, envKeyLimaHome, mockLimaHomePath) + cmd := mocks.NewCommand(ctrl) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Stdin().Return(nil) + lcd.EXPECT().Stderr().Return(nil) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + var stdoutBuf *bytes.Buffer + cmd.EXPECT().SetStdout(gomock.Any()).Do(func(buf *bytes.Buffer) { + stdoutBuf = buf + }) + cmd.EXPECT().SetStderr(nil) + cmd.EXPECT().Run().Do(func() { + stdoutBuf.Write([]byte(inOut)) + }) + lcd.EXPECT().Stdout().Return(f) + }, + }, + { + name: "overlapped replacements", + wantErr: nil, + stdoutRs: []command.Replacement{{Source: "s1", Target: "s2"}, {Source: "s2", Target: "s3"}}, + inOut: "s1 s2 ,s3 /s4 s1.s5", + outOut: "s3 s3 ,s3 /s4 s3.s5", + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, + lcd *mocks.LimaCmdCreatorSystemDeps, ctrl *gomock.Controller, inOut string, f *os.File, + ) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", + mockArgs, envKeyLimaHome, mockLimaHomePath) + cmd := mocks.NewCommand(ctrl) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Stdin().Return(nil) + lcd.EXPECT().Stderr().Return(nil) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + var stdoutBuf *bytes.Buffer + cmd.EXPECT().SetStdout(gomock.Any()).Do(func(buf *bytes.Buffer) { + stdoutBuf = buf + }) + cmd.EXPECT().SetStderr(nil) + cmd.EXPECT().Run().Do(func() { + stdoutBuf.Write([]byte(inOut)) + }) + lcd.EXPECT().Stdout().Return(f) + }, + }, + { + name: "empty replacements", + wantErr: nil, + stdoutRs: []command.Replacement{}, + inOut: "s1 s2 ,s3 /s4 .s5", + outOut: "s1 s2 ,s3 /s4 .s5", + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, + lcd *mocks.LimaCmdCreatorSystemDeps, ctrl *gomock.Controller, inOut string, f *os.File, + ) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", mockArgs, envKeyLimaHome, mockLimaHomePath) + cmd := mocks.NewCommand(ctrl) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Stdin().Return(nil) + lcd.EXPECT().Stderr().Return(nil) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + var stdoutBuf *bytes.Buffer + cmd.EXPECT().SetStdout(gomock.Any()).Do(func(buf *bytes.Buffer) { + stdoutBuf = buf + }) + cmd.EXPECT().SetStderr(nil) + cmd.EXPECT().Run().Do(func() { + stdoutBuf.Write([]byte(inOut)) + }) + lcd.EXPECT().Stdout().Return(f) + }, + }, + { + name: "running cmd returns error", + wantErr: errors.New("run cmd error"), + stdoutRs: []command.Replacement{{Source: "source-out", Target: "target-out"}}, + inOut: "source-out", + outOut: "", + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, + lcd *mocks.LimaCmdCreatorSystemDeps, ctrl *gomock.Controller, inOut string, f *os.File, + ) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", mockArgs, envKeyLimaHome, mockLimaHomePath) + cmd := mocks.NewCommand(ctrl) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Stdin().Return(nil) + lcd.EXPECT().Stderr().Return(nil) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + cmd.EXPECT().SetStdout(gomock.Any()) + cmd.EXPECT().SetStderr(nil) + cmd.EXPECT().Run().Return(errors.New("run cmd error")) + }, + }, + { + name: "writing to stdout returns error", + wantErr: fs.ErrInvalid, + stdoutRs: []command.Replacement{{Source: "source-out", Target: "target-out"}}, + inOut: "source-out", + outOut: "", + mockSvc: func(logger *mocks.Logger, cmdCreator *mocks.CommandCreator, + lcd *mocks.LimaCmdCreatorSystemDeps, ctrl *gomock.Controller, inOut string, f *os.File, + ) { + logger.EXPECT().Debugf("Creating limactl command: ARGUMENTS: %v, %s: %s", mockArgs, envKeyLimaHome, mockLimaHomePath) + cmd := mocks.NewCommand(ctrl) + cmdCreator.EXPECT().Create(mockLimactlPath, mockArgs).Return(cmd) + lcd.EXPECT().Environ().Return([]string{}) + lcd.EXPECT().Stdin().Return(nil) + lcd.EXPECT().Stderr().Return(nil) + lcd.EXPECT().Env(envKeyPath).Return(mockSystemPath) + cmd.EXPECT().SetEnv([]string{ + fmt.Sprintf("%s=%s", envKeyLimaHome, mockLimaHomePath), + fmt.Sprintf("%s=%s", envKeyPath, finalPath), + }) + cmd.EXPECT().SetStdin(nil) + var stdoutBuf *bytes.Buffer + cmd.EXPECT().SetStdout(gomock.Any()).Do(func(buf *bytes.Buffer) { + stdoutBuf = buf + }) + cmd.EXPECT().SetStderr(nil) + cmd.EXPECT().Run().Do(func() { + stdoutBuf.Write([]byte(inOut)) + }) + lcd.EXPECT().Stdout().Return(nil) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmdCreator := mocks.NewCommandCreator(ctrl) + logger := mocks.NewLogger(ctrl) + lcd := mocks.NewLimaCmdCreatorSystemDeps(ctrl) + + stdoutFilepath := filepath.Clean(filepath.Join(t.TempDir(), "test")) + stdoutFile, err := os.Create(stdoutFilepath) + require.NoError(t, err) + + tc.mockSvc(logger, cmdCreator, lcd, ctrl, tc.inOut, stdoutFile) + assert.Equal(t, tc.wantErr, command.NewLimaCmdCreator(cmdCreator, logger, mockLimaHomePath, mockLimactlPath, mockQemuBinPath, lcd). + RunWithReplacingStdout(tc.stdoutRs, mockArgs...)) + + stdout, err := os.ReadFile(stdoutFilepath) + require.NoError(t, err) + assert.Equal(t, tc.outOut, string(stdout)) + }) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 000000000..df952f643 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,130 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package config handles parsing and applying options from finch's config +// file. These options can be applied to any aspect of the project, from the VMM +// to components running inside the VM. +// +// Currently, VMM options are applied to one of Lima's configuration files and options +// within the VM are applied via running SSH commands and writing files via SFTP. +package config + +import ( + "errors" + "fmt" + "path" + + "github.com/spf13/afero" + "gopkg.in/yaml.v3" + + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/fmemory" + "github.com/runfinch/finch/pkg/system" +) + +// Finch represents the configuration file for Finch CLI. +type Finch struct { + CPUs *int `yaml:"cpus"` + Memory *string `yaml:"memory"` +} + +// Nerdctl is a copy from github.com/containerd/nerdctl/cmd/nerdctl/main.go +// TODO: make PR to nerdctl repo to move this config out of the main package +// so it can be imported on macOS. +type Nerdctl struct { + Debug bool `toml:"debug,omitempty"` + DebugFull bool `toml:"debug_full1,omitempty"` + Address string `toml:"address,omitempty"` + Namespace string `toml:"namespace,omitempty"` + Snapshotter string `toml:"snapshotter,omitempty"` + CNIPath string `toml:"cni_path,omitempty"` + CNINetConfPath string `toml:"cni_netconfpath,omitempty"` + DataRoot string `toml:"data_root,omitempty"` + CgroupManager string `toml:"cgroup_manager,omitempty"` + InsecureRegistry bool `toml:"insecure_registry,omitempty"` + HostsDir []string `toml:"hosts_dir,omitempty"` +} + +// LimaConfigApplier applies lima configuration changes. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_config_lima_config_applier.go -package=mocks -mock_names LimaConfigApplier=LimaConfigApplier . LimaConfigApplier +type LimaConfigApplier interface { + Apply() error +} + +// NerdctlConfigApplier applies nerdctl configuration changes. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_config_nerdctl_config_applier.go -package=mocks -mock_names NerdctlConfigApplier=NerdctlConfigApplier . NerdctlConfigApplier +type NerdctlConfigApplier interface { + Apply(remoteAddr string) error +} + +// LoadSystemDeps contains the system dependencies for Load. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_config_load_system_deps.go -package=mocks -mock_names LoadSystemDeps=LoadSystemDeps . LoadSystemDeps +type LoadSystemDeps interface { + system.RuntimeCPUGetter +} + +// writeConfig writes a config struct back to a YAML file at a path. +func writeConfig(cfg *Finch, fs afero.Fs, path string) error { + cfgBuf, err := yaml.Marshal(cfg) + if err != nil { + return fmt.Errorf("failed to write to marshal config: %w", err) + } + + if err := afero.WriteFile(fs, path, cfgBuf, 0o755); err != nil { + return fmt.Errorf("failed to write to config file: %w", err) + } + + return nil +} + +func ensureConfigDir(fs afero.Fs, path string, log flog.Logger) error { + dirExists, err := afero.DirExists(fs, path) + if err != nil { + return fmt.Errorf("failed to get status of config directory: %w", err) + } + if !dirExists { + log.Infof("%q directory doesn't exist, attempting to create it", path) + if err := fs.Mkdir(path, 0o755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + } + return nil +} + +// Load loads Finch's configuration from a YAML file and initializes default values. +func Load(fs afero.Fs, cfgPath string, log flog.Logger, systemDeps LoadSystemDeps, mem fmemory.Memory) (*Finch, error) { + b, err := afero.ReadFile(fs, cfgPath) + if err != nil { + if errors.Is(err, afero.ErrFileNotFound) { + log.Infof("Using default values due to missing config file at %q", cfgPath) + defCfg := applyDefaults(&Finch{}, systemDeps, mem) + if err := ensureConfigDir(fs, path.Dir(cfgPath), log); err != nil { + return nil, fmt.Errorf("failed to ensure %q directory: %w", cfgPath, err) + } + if err := writeConfig(defCfg, fs, cfgPath); err != nil { + return nil, err + } + return defCfg, nil + } + return nil, fmt.Errorf("failed to read the config file: %w", err) + } + + var cfg Finch + if err := yaml.Unmarshal(b, &cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal config file, using default values: %w", err) + } + + defCfg := applyDefaults(&cfg, systemDeps, mem) + if err := writeConfig(defCfg, fs, cfgPath); err != nil { + return nil, err + } + + if err := validate(defCfg, log, systemDeps, mem); err != nil { + return nil, fmt.Errorf("failed to validate config file: %w", err) + } + + return defCfg, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..57da3151b --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,239 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "errors" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + "gopkg.in/yaml.v3" + + "github.com/runfinch/finch/pkg/mocks" +) + +func TestLoad(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + path string + mockSvc func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) + want *Finch + wantErr error + }{ + { + name: "happy path", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + data := ` +memory: 4GiB +cpus: 8 +` + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte(data), 0o600)) + deps.EXPECT().NumCPU().Return(8) + // 12_884_901_888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + }, + want: &Finch{Memory: pointer.String("4GiB"), CPUs: pointer.Int(8)}, + wantErr: nil, + }, + { + name: "config file exists, but is empty", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte(""), 0o600)) + deps.EXPECT().NumCPU().Return(4).Times(2) + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(2) + }, + want: &Finch{Memory: pointer.String("3GiB"), CPUs: pointer.Int(2)}, + wantErr: nil, + }, + { + name: "config file exists, but contains only some fields", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("memory: 2GiB"), 0o600)) + deps.EXPECT().NumCPU().Return(4).Times(2) + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(1) + }, + want: &Finch{Memory: pointer.String("2GiB"), CPUs: pointer.Int(2)}, + wantErr: nil, + }, + { + name: "config file exists, but contains an unknown field", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("unknownField: 2GiB"), 0o600)) + deps.EXPECT().NumCPU().Return(4).Times(2) + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(2) + }, + want: &Finch{Memory: pointer.String("3GiB"), CPUs: pointer.Int(2)}, + wantErr: nil, + }, + { + name: "config file does not exist", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + l.EXPECT().Infof("Using default values due to missing config file at %q", "/config.yaml") + deps.EXPECT().NumCPU().Return(4).Times(1) + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)).Times(1) + }, + want: &Finch{Memory: pointer.String("3GiB"), CPUs: pointer.Int(2)}, + wantErr: nil, + }, + { + name: "config file does not contain valid YAML", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte("this isn't YAML"), 0o600)) + }, + want: nil, + wantErr: fmt.Errorf( + "failed to unmarshal config file, using default values: %w", + &yaml.TypeError{Errors: []string{"line 1: cannot unmarshal !!str `this is...` into config.Finch"}}, + ), + }, + { + name: "config file doesn't pass validation", + path: "/config.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + require.NoError(t, afero.WriteFile(fs, "/config.yaml", []byte(`memory: 4GiB +cpus: 0 +`, + ), 0o600)) + }, + want: nil, + wantErr: fmt.Errorf( + "failed to validate config file: %w", + errors.New("specified number of CPUs (0) must be greater than 0"), + ), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + l := mocks.NewLogger(ctrl) + deps := mocks.NewLoadSystemDeps(ctrl) + mem := mocks.NewMemory(ctrl) + fs := afero.NewMemMapFs() + + tc.mockSvc(fs, l, deps, mem) + + got, gotErr := Load(fs, tc.path, l, deps, mem) + require.Equal(t, tc.wantErr, gotErr) + assert.Equal(t, tc.want, got) + }) + } +} + +func Test_writeConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cfg *Finch + path string + mockSvc func(t *testing.T, fs afero.Fs) + postRunCheck func(t *testing.T, fs afero.Fs) + err error + }{ + { + name: "happy path", + cfg: &Finch{ + CPUs: pointer.Int(4), + Memory: pointer.String("4GiB"), + }, + path: "/config.yaml", + mockSvc: func(t *testing.T, fs afero.Fs) {}, + err: nil, + postRunCheck: func(t *testing.T, fs afero.Fs) { + b, err := afero.ReadFile(fs, "/config.yaml") + require.NoError(t, err) + + require.Equal(t, b, []byte("cpus: 4\nmemory: 4GiB\n")) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + + tc.mockSvc(t, fs) + + err := writeConfig(tc.cfg, fs, tc.path) + require.Equal(t, tc.err, err) + + tc.postRunCheck(t, fs) + }) + } +} + +func Test_ensureConfigDir(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + path string + mockSvc func(t *testing.T, fs afero.Fs, l *mocks.Logger) + postRunCheck func(t *testing.T, fs afero.Fs) + err error + }{ + { + name: "happy path", + path: "/.finch", + mockSvc: func(t *testing.T, fs afero.Fs, l *mocks.Logger) { + require.NoError(t, fs.Mkdir("/.finch", 0o600)) + }, + err: nil, + postRunCheck: func(t *testing.T, fs afero.Fs) { + _, err := afero.DirExists(fs, "/.finch") + require.NoError(t, err) + }, + }, + { + name: "directory doesn't exist", + path: "/.finch", + mockSvc: func(t *testing.T, fs afero.Fs, l *mocks.Logger) { + l.EXPECT().Infof("%q directory doesn't exist, attempting to create it", "/.finch") + }, + err: nil, + postRunCheck: func(t *testing.T, fs afero.Fs) { + _, err := afero.DirExists(fs, "/.finch") + require.NoError(t, err) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + fs := afero.NewMemMapFs() + l := mocks.NewLogger(ctrl) + + tc.mockSvc(t, fs, l) + + err := ensureConfigDir(fs, tc.path, l) + require.Equal(t, tc.err, err) + + tc.postRunCheck(t, fs) + }) + } +} diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go new file mode 100644 index 000000000..c9ac9bfdb --- /dev/null +++ b/pkg/config/defaults.go @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "math" + + "github.com/docker/go-units" + "github.com/xorcare/pointer" + + "github.com/runfinch/finch/pkg/fmemory" +) + +const ( + // 2,147,483,648 => 2GiB. + fallbackMemory float64 = 2_147_483_648 + fallbackCPUs int = 2 +) + +// applyDefaults sets default configuration options if they are not already set. +func applyDefaults(cfg *Finch, deps LoadSystemDeps, mem fmemory.Memory) *Finch { + if cfg.CPUs == nil { + defaultCPUs := int(math.Round(float64(deps.NumCPU()) * 0.25)) + if defaultCPUs >= fallbackCPUs { + cfg.CPUs = pointer.Int(defaultCPUs) + } else { + cfg.CPUs = pointer.Int(fallbackCPUs) + } + } + + if cfg.Memory == nil { + defaultMemory := math.Round(float64(mem.TotalMemory()) * 0.25) + if defaultMemory >= fallbackMemory { + cfg.Memory = pointer.String(units.BytesSize(defaultMemory)) + } else { + cfg.Memory = pointer.String(units.BytesSize(fallbackMemory)) + } + } + + return cfg +} diff --git a/pkg/config/defaults_test.go b/pkg/config/defaults_test.go new file mode 100644 index 000000000..2500399d5 --- /dev/null +++ b/pkg/config/defaults_test.go @@ -0,0 +1,95 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + + "github.com/runfinch/finch/pkg/mocks" +) + +func Test_applyDefaults(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cfg *Finch + mockSvc func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) + want *Finch + }{ + { + name: "happy path", + cfg: &Finch{}, + mockSvc: func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + deps.EXPECT().NumCPU().Return(8) + // 12,884,901,888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + }, + want: &Finch{ + CPUs: pointer.Int(2), + Memory: pointer.String("3GiB"), + }, + }, + { + name: "fills CPUs with default when unset", + cfg: &Finch{ + Memory: pointer.String("4GiB"), + }, + mockSvc: func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + deps.EXPECT().NumCPU().Return(8) + }, + want: &Finch{ + CPUs: pointer.Int(2), + Memory: pointer.String("4GiB"), + }, + }, + { + name: "fills memory with default when unset", + cfg: &Finch{ + CPUs: pointer.Int(6), + }, + mockSvc: func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + // 12,884,901,888 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_884_901_888)) + }, + want: &Finch{ + CPUs: pointer.Int(6), + Memory: pointer.String("3GiB"), + }, + }, + { + name: "fills with fallbacks when defaults are too low", + cfg: &Finch{}, + mockSvc: func(deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + deps.EXPECT().NumCPU().Return(4) + // 1,073,741,824 == 1GiB + mem.EXPECT().TotalMemory().Return(uint64(1_073_741_824)) + }, + want: &Finch{ + CPUs: pointer.Int(2), + Memory: pointer.String("2GiB"), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + deps := mocks.NewLoadSystemDeps(ctrl) + mem := mocks.NewMemory(ctrl) + + tc.mockSvc(deps, mem) + + got := applyDefaults(tc.cfg, deps, mem) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/config/lima_config_applier.go b/pkg/config/lima_config_applier.go new file mode 100644 index 000000000..274cc0f2b --- /dev/null +++ b/pkg/config/lima_config_applier.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "fmt" + + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/spf13/afero" + "gopkg.in/yaml.v3" +) + +type limaConfigApplier struct { + cfg *Finch + fs afero.Fs + limaConfigPath string +} + +var _ LimaConfigApplier = (*limaConfigApplier)(nil) + +// NewLimaApplier creates a new LimaConfigApplier that +// applies lima configuration changes by writing to the lima config file on the disk. +func NewLimaApplier(cfg *Finch, fs afero.Fs, limaConfigPath string) LimaConfigApplier { + return &limaConfigApplier{ + cfg: cfg, + fs: fs, + limaConfigPath: limaConfigPath, + } +} + +// Apply reads the Finch config from disk and writes the Lima-related portion to the lima config file. +func (lca *limaConfigApplier) Apply() error { + b, err := afero.ReadFile(lca.fs, lca.limaConfigPath) + if err != nil { + return fmt.Errorf("failed to load the lima config file: %w", err) + } + + var limaCfg limayaml.LimaYAML + if err := yaml.Unmarshal(b, &limaCfg); err != nil { + return fmt.Errorf("failed to unmarshal the lima config file: %w", err) + } + + limaCfg.CPUs = lca.cfg.CPUs + limaCfg.Memory = lca.cfg.Memory + + limaCfgBytes, err := yaml.Marshal(limaCfg) + if err != nil { + return fmt.Errorf("failed to marshal the lima config file: %w", err) + } + + if err := afero.WriteFile(lca.fs, lca.limaConfigPath, limaCfgBytes, 0o644); err != nil { + return fmt.Errorf("failed to write to the lima config file: %w", err) + } + + return nil +} diff --git a/pkg/config/lima_config_applier_test.go b/pkg/config/lima_config_applier_test.go new file mode 100644 index 000000000..d2d8a53c9 --- /dev/null +++ b/pkg/config/lima_config_applier_test.go @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "errors" + "fmt" + "io/fs" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + "gopkg.in/yaml.v3" + + "github.com/runfinch/finch/pkg/mocks" +) + +func TestDiskLimaConfigApplier_Apply(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + config *Finch + path string + mockSvc func(fs afero.Fs, l *mocks.Logger) + postRunCheck func(t *testing.T, fs afero.Fs) + want error + }{ + { + name: "happy path", + config: &Finch{ + Memory: pointer.String("2GiB"), + CPUs: pointer.Int(4), + }, + path: "/lima.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger) { + err := afero.WriteFile(fs, "/lima.yaml", []byte("memory: 4GiB\ncpus: 8"), 0o600) + require.NoError(t, err) + }, + postRunCheck: func(t *testing.T, fs afero.Fs) { + buf, err := afero.ReadFile(fs, "/lima.yaml") + require.NoError(t, err) + + // limayaml.LimaYAML has a required "images" field which will also get marshaled + require.Equal(t, buf, []byte("images: []\ncpus: 4\nmemory: 2GiB\n")) + }, + want: nil, + }, + { + name: "lima config file does not exist", + config: nil, + path: "/lima.yaml", + mockSvc: func(afs afero.Fs, l *mocks.Logger) {}, + postRunCheck: func(t *testing.T, mFs afero.Fs) { + _, err := afero.ReadFile(mFs, "/lima.yaml") + require.Equal(t, err, &fs.PathError{Op: "open", Path: "/lima.yaml", Err: errors.New("file does not exist")}) + }, + want: fmt.Errorf("failed to load the lima config file: %w", + &fs.PathError{Op: "open", Path: "/lima.yaml", Err: errors.New("file does not exist")}, + ), + }, + { + name: "lima config file does not contain valid YAML", + config: nil, + path: "/lima.yaml", + mockSvc: func(fs afero.Fs, l *mocks.Logger) { + err := afero.WriteFile(fs, "/lima.yaml", []byte("this isn't YAML"), 0o600) + require.NoError(t, err) + }, + postRunCheck: func(t *testing.T, fs afero.Fs) { + buf, err := afero.ReadFile(fs, "/lima.yaml") + require.NoError(t, err) + + require.Equal(t, buf, []byte("this isn't YAML")) + }, + want: fmt.Errorf( + "failed to unmarshal the lima config file: %w", + &yaml.TypeError{Errors: []string{"line 1: cannot unmarshal !!str `this is...` into limayaml.LimaYAML"}}, + ), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + l := mocks.NewLogger(ctrl) + fs := afero.NewMemMapFs() + + tc.mockSvc(fs, l) + got := NewLimaApplier(tc.config, fs, tc.path).Apply() + + require.Equal(t, tc.want, got) + tc.postRunCheck(t, fs) + }) + } +} diff --git a/pkg/config/nerdctl_config_applier.go b/pkg/config/nerdctl_config_applier.go new file mode 100644 index 000000000..80a4cb9a0 --- /dev/null +++ b/pkg/config/nerdctl_config_applier.go @@ -0,0 +1,156 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "errors" + "fmt" + "path" + "strings" + + toml "github.com/pelletier/go-toml" + "github.com/pkg/sftp" + "github.com/spf13/afero" + "github.com/spf13/afero/sftpfs" + + "github.com/runfinch/finch/pkg/fssh" + "github.com/runfinch/finch/pkg/system" +) + +const ( + nerdctlNamespace = "finch" + nerdctlRootfulCfgPath = "/etc/nerdctl/nerdctl.toml" +) + +// NerdctlConfigApplierSystemDeps contains the system dependencies required for NewNerdctlApplier. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_config_nerdctl_config_applier_system_deps.go -package=mocks -mock_names NerdctlConfigApplierSystemDeps=NerdctlConfigApplierSystemDeps . NerdctlConfigApplierSystemDeps +type NerdctlConfigApplierSystemDeps interface { + system.EnvGetter +} + +type nerdctlConfigApplier struct { + dialer fssh.Dialer + fs afero.Fs + privateKeyPath string + systemDeps NerdctlConfigApplierSystemDeps +} + +var _ NerdctlConfigApplier = (*nerdctlConfigApplier)(nil) + +// NewNerdctlApplier creates a new NerdctlConfigApplier that +// applies nerdctl configuration changes by SSHing to the lima VM to update the nerdctl configuration file in it. +func NewNerdctlApplier(dialer fssh.Dialer, fs afero.Fs, privateKeyPath string, systemDeps NerdctlConfigApplierSystemDeps) NerdctlConfigApplier { + return &nerdctlConfigApplier{ + dialer: dialer, + fs: fs, + privateKeyPath: privateKeyPath, + systemDeps: systemDeps, + } +} + +// updateEnvironment adds variables to the user's shell's environment. Currently it uses ~/.bashrc because +// Bash is the default shell and Bash will not load ~/.profile if ~/.bash_profile exists (which it does). +// ~/.bash_profile sources ~/.bashrc, so ~/.bashrc is currently the best place to define additional variables. +// The [GNU docs for Bash] explain how these files work together in more details. +// The default location of DOCKER_CONFIG is ~/.docker/config.json. This config change sets the location to +// ~/.finch/config.json, but from the perspective of macOS (/Users//.finch/config.json). +// For more information on the variable, see the registry nerdctl docs. +// +// [GNU docs for Bash]: https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html +// +// [registry nerdctl docs]: https://github.com/containerd/nerdctl/blob/master/docs/registry.md +func updateEnvironment(fs afero.Fs, user string) error { + profileFilePath := fmt.Sprintf("/home/%s.linux/.bashrc", user) + profBuf, err := afero.ReadFile(fs, profileFilePath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + profStr := string(profBuf) + if !strings.Contains(profStr, "export DOCKER_CONFIG") { + profBufWithDockerCfg := fmt.Sprintf("%s\nexport DOCKER_CONFIG=\"/Users/%s/.finch\"\n", profStr, user) + if err := afero.WriteFile(fs, profileFilePath, []byte(profBufWithDockerCfg), 0o644); err != nil { + return fmt.Errorf("failed to write to profile file: %w", err) + } + } + + return nil +} + +// updateNerdctlConfig reads from the nerdctl config and updates values. +func updateNerdctlConfig(fs afero.Fs, user string, rootless bool) error { + nerdctlRootlessCfgPath := fmt.Sprintf("/home/%s.linux/.config/nerdctl/nerdctl.toml", user) + + var cfgPath string + if rootless { + cfgPath = nerdctlRootlessCfgPath + } else { + cfgPath = nerdctlRootfulCfgPath + } + + if err := fs.MkdirAll(path.Dir(cfgPath), 0o755); err != nil { + return fmt.Errorf("failed to create config dir (dir(filepath)) %s: %w", cfgPath, err) + } + + if _, err := fs.Stat(cfgPath); errors.Is(err, afero.ErrFileNotFound) { + if err := afero.WriteFile(fs, cfgPath, []byte{}, 0o644); err != nil { + return fmt.Errorf("failed to create %q with afero.WriteFile: %w", cfgPath, err) + } + } + + var cfg Nerdctl + cfgBuf, err := afero.ReadFile(fs, cfgPath) + if err != nil { + return fmt.Errorf("failed to read config file %s: %w", cfgPath, err) + } + + if err := toml.Unmarshal(cfgBuf, &cfg); err != nil { + return fmt.Errorf("failed to unmarshal config file %s: %w", cfgPath, err) + } + + cfg.Namespace = nerdctlNamespace + + updatedCfg, err := toml.Marshal(cfg) + if err != nil { + return fmt.Errorf("failed to marshal config file %s: %w", cfgPath, err) + } + + if err := afero.WriteFile(fs, cfgPath, updatedCfg, 0o644); err != nil { + return fmt.Errorf("failed to write to config file %s: %w", cfgPath, err) + } + + return nil +} + +// Apply gets SSH and SFTP clients and uses them to update the nerdctl config. +func (nca *nerdctlConfigApplier) Apply(remoteAddr string) error { + user := nca.systemDeps.Env("USER") + sshCfg, err := fssh.NewClientConfig(nca.fs, user, nca.privateKeyPath) + if err != nil { + return fmt.Errorf("failed to create ssh client config: %w", err) + } + + sshClient, err := nca.dialer.Dial("tcp", remoteAddr, sshCfg) + if err != nil { + return fmt.Errorf("failed to setup ssh client: %w", err) + } + + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + return fmt.Errorf("failed to setup sftp client: %w", err) + } + + sftpFs := sftpfs.New(sftpClient) + + // rootless hardcoded to true for now to match our os.yaml file + if err := updateNerdctlConfig(sftpFs, user, true); err != nil { + return fmt.Errorf("failed to update the nerdctl config file: %w", err) + } + + if err := updateEnvironment(sftpFs, user); err != nil { + return fmt.Errorf("failed to update the user's .profile file: %w", err) + } + return nil +} diff --git a/pkg/config/nerdctl_config_applier_test.go b/pkg/config/nerdctl_config_applier_test.go new file mode 100644 index 000000000..cd6302e29 --- /dev/null +++ b/pkg/config/nerdctl_config_applier_test.go @@ -0,0 +1,243 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "errors" + "fmt" + "io/fs" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/runfinch/finch/pkg/mocks" +) + +// disclaimer: this key has never been and should never be used for anything +var fakeSSHKey = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAfR367TtAGV+abvj4pRDcFdU2enKE+iC4qF3LNJF9eyQAAAKjEIxhXxCMY +VwAAAAtzc2gtZWQyNTUxOQAAACAfR367TtAGV+abvj4pRDcFdU2enKE+iC4qF3LNJF9eyQ +AAAEANzWA32dcyDqkfg7zbzt7D76PTyyaX0n1/goKJNPLYyB9HfrtO0AZX5pu+PilENwV1 +TZ6coT6ILioXcs0kX17JAAAAI2FsdmFqdXNAODg2NjVhMGJmN2NhLmFudC5hbWF6b24uY2 +9tAQI= +-----END OPENSSH PRIVATE KEY-----` + +func Test_updateEnvironment(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + user string + mockSvc func(t *testing.T, fs afero.Fs) + postRunCheck func(t *testing.T, fs afero.Fs) + want error + }{ + { + name: "happy path", + user: "mock_user", + mockSvc: func(t *testing.T, fs afero.Fs) { + require.NoError(t, afero.WriteFile(fs, "/home/mock_user.linux/.bashrc", []byte(""), 0o644)) + }, + postRunCheck: func(t *testing.T, fs afero.Fs) { + fileBytes, err := afero.ReadFile(fs, "/home/mock_user.linux/.bashrc") + require.NoError(t, err) + assert.Equal(t, []byte("\n"+`export DOCKER_CONFIG="/Users/mock_user/.finch"`+"\n"), fileBytes) + }, + want: nil, + }, + { + name: "happy path, file already exists and already contains expected variables", + user: "mock_user", + mockSvc: func(t *testing.T, fs afero.Fs) { + require.NoError( + t, + afero.WriteFile( + fs, + "/home/mock_user.linux/.bashrc", + []byte(`export DOCKER_CONFIG="/Users/mock_user/.finch"`), + 0o644, + ), + ) + }, + postRunCheck: func(t *testing.T, fs afero.Fs) { + fileBytes, err := afero.ReadFile(fs, "/home/mock_user.linux/.bashrc") + require.NoError(t, err) + assert.Equal(t, []byte(`export DOCKER_CONFIG="/Users/mock_user/.finch"`), fileBytes) + }, + want: nil, + }, + { + name: ".bashrc file doesn't exist", + user: "mock_user", + mockSvc: func(t *testing.T, fs afero.Fs) {}, + postRunCheck: func(t *testing.T, fs afero.Fs) {}, + want: fmt.Errorf( + "failed to read config file: %w", + &fs.PathError{Op: "open", Path: "/home/mock_user.linux/.bashrc", Err: errors.New("file does not exist")}, + ), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + + tc.mockSvc(t, fs) + got := updateEnvironment(fs, tc.user) + require.Equal(t, tc.want, got) + + tc.postRunCheck(t, fs) + }) + } +} + +func Test_updateNerdctlConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + user string + rootless bool + mockSvc func(t *testing.T, fs afero.Fs) + postRunCheck func(t *testing.T, fs afero.Fs) + want error + }{ + { + name: "happy path, rootless", + user: "mock_user", + rootless: true, + mockSvc: func(t *testing.T, fs afero.Fs) {}, + postRunCheck: func(t *testing.T, fs afero.Fs) { + fileBytes, err := afero.ReadFile(fs, "/home/mock_user.linux/.config/nerdctl/nerdctl.toml") + require.NoError(t, err) + assert.Equal(t, []byte(`namespace = "finch"`+"\n"), fileBytes) + }, + want: nil, + }, + { + name: "happy path, rootful", + user: "mock_user", + rootless: false, + mockSvc: func(t *testing.T, fs afero.Fs) {}, + postRunCheck: func(t *testing.T, fs afero.Fs) { + fileBytes, err := afero.ReadFile(fs, "/etc/nerdctl/nerdctl.toml") + require.NoError(t, err) + assert.Equal(t, []byte(`namespace = "finch"`+"\n"), fileBytes) + }, + want: nil, + }, + { + name: "happy path, config already exists", + user: "mock_user", + rootless: true, + mockSvc: func(t *testing.T, fs afero.Fs) { + err := afero.WriteFile(fs, "/home/mock_user.linux/.config/nerdctl/nerdctl.toml", + []byte(`namespace = "something-else"`), 0o644) + require.NoError(t, err) + }, + postRunCheck: func(t *testing.T, fs afero.Fs) { + fileBytes, err := afero.ReadFile(fs, "/home/mock_user.linux/.config/nerdctl/nerdctl.toml") + require.NoError(t, err) + assert.Equal(t, []byte(`namespace = "finch"`+"\n"), fileBytes) + }, + want: nil, + }, + { + name: "config contains invalid TOML", + user: "mock_user", + rootless: true, + mockSvc: func(t *testing.T, fs afero.Fs) { + err := afero.WriteFile(fs, "/home/mock_user.linux/.config/nerdctl/nerdctl.toml", []byte("{not toml}"), 0o644) + require.NoError(t, err) + }, + postRunCheck: func(t *testing.T, fs afero.Fs) {}, + want: fmt.Errorf( + "failed to unmarshal config file %s: %w", + "/home/mock_user.linux/.config/nerdctl/nerdctl.toml", + errors.New("(1, 1): parsing error: keys cannot contain { character"), + ), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + + tc.mockSvc(t, fs) + got := updateNerdctlConfig(fs, tc.user, tc.rootless) + require.Equal(t, tc.want, got) + + tc.postRunCheck(t, fs) + }) + } +} + +func TestNerdctlConfigApplier_Apply(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + path string + remoteAddr string + mockSvc func(t *testing.T, fs afero.Fs, d *mocks.Dialer, sd *mocks.NerdctlConfigApplierSystemDeps) + want error + }{ + { + name: "private key path doesn't exist", + path: "/private-key", + remoteAddr: "", + mockSvc: func(t *testing.T, fs afero.Fs, d *mocks.Dialer, sd *mocks.NerdctlConfigApplierSystemDeps) { + sd.EXPECT().Env("USER").Return("user") + }, + want: fmt.Errorf( + "failed to create ssh client config: %w", + fmt.Errorf( + "failed to open private key file: %w", + &fs.PathError{Op: "open", Path: "/private-key", Err: errors.New("file does not exist")}, + ), + ), + }, + { + name: "dialer fails to create the ssh connection", + path: "/private-key", + remoteAddr: "deadbeef", + mockSvc: func(t *testing.T, fs afero.Fs, d *mocks.Dialer, sd *mocks.NerdctlConfigApplierSystemDeps) { + err := afero.WriteFile(fs, "/private-key", []byte(fakeSSHKey), 0o600) + require.NoError(t, err) + + sd.EXPECT().Env("USER").Return("user") + d.EXPECT().Dial("tcp", "deadbeef", gomock.Any()).Return(nil, fmt.Errorf("some error")) + }, + want: fmt.Errorf("failed to setup ssh client: %w", fmt.Errorf("some error")), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + fs := afero.NewMemMapFs() + d := mocks.NewDialer(ctrl) + sd := mocks.NewNerdctlConfigApplierSystemDeps(ctrl) + + tc.mockSvc(t, fs, d, sd) + got := NewNerdctlApplier(d, fs, tc.path, sd).Apply(tc.remoteAddr) + + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/config/validate.go b/pkg/config/validate.go new file mode 100644 index 000000000..9ab3d6cce --- /dev/null +++ b/pkg/config/validate.go @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "fmt" + + "github.com/docker/go-units" + + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/fmemory" +) + +func validate(cfg *Finch, log flog.Logger, systemDeps LoadSystemDeps, mem fmemory.Memory) error { + if *cfg.CPUs <= 0 { + return fmt.Errorf( + "specified number of CPUs (%d) must be greater than 0", + *cfg.CPUs, + ) + } + + memInt, err := units.FromHumanSize(*cfg.Memory) + if err != nil { + return fmt.Errorf("failed to parse memory to uint: %w", err) + } + + if memInt <= 0 { + return fmt.Errorf( + "specified amount of memory (%s) must be greater than 0GiB", + *cfg.Memory, + ) + } + + totalCPUs := systemDeps.NumCPU() + if *cfg.CPUs > totalCPUs { + log.Infof( + "The specified number of CPUs (%d) is greater than CPUs available on this system (%d),\n"+ + "which may lead to severe performance degradation", + *cfg.CPUs, + totalCPUs, + ) + } + + totalMem := mem.TotalMemory() + if uint64(memInt) > totalMem { + log.Infof( + "The specified amount of memory (%s) is greater than the memory available on this system (%s),\n"+ + "which may lead to severe performance degradation", + *cfg.Memory, + units.BytesSize(float64(totalMem)), + ) + } + + return nil +} diff --git a/pkg/config/validate_test.go b/pkg/config/validate_test.go new file mode 100644 index 000000000..d355fce96 --- /dev/null +++ b/pkg/config/validate_test.go @@ -0,0 +1,112 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/xorcare/pointer" + + "github.com/runfinch/finch/pkg/mocks" +) + +func TestValidate(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cfg *Finch + mockSvc func(l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) + err error + }{ + { + name: "happy path", + cfg: &Finch{ + CPUs: pointer.Int(4), + Memory: pointer.String("4GiB"), + }, + mockSvc: func(l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + deps.EXPECT().NumCPU().Return(8) + // 12,880,000,000 == 12GiB + mem.EXPECT().TotalMemory().Return(uint64(12_880_000_000)) + }, + err: nil, + }, + { + name: "config specifies less CPUs than required", + cfg: &Finch{ + CPUs: pointer.Int(0), + Memory: pointer.String("0GiB"), + }, + mockSvc: func(l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) {}, + err: errors.New("specified number of CPUs (0) must be greater than 0"), + }, + { + name: "config specifies less memory than required", + cfg: &Finch{ + CPUs: pointer.Int(1), + Memory: pointer.String("0GiB"), + }, + mockSvc: func(l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) {}, + err: errors.New("specified amount of memory (0GiB) must be greater than 0GiB"), + }, + { + name: "config specifies more CPUs than available", + cfg: &Finch{ + CPUs: pointer.Int(4), + Memory: pointer.String("4GiB"), + }, + mockSvc: func(l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + deps.EXPECT().NumCPU().Return(1) + mem.EXPECT().TotalMemory().Return(uint64(12_880_000_000)) + l.EXPECT().Infof( + "The specified number of CPUs (%d) is greater than CPUs available on this system (%d),\n"+ + "which may lead to severe performance degradation", + 4, + 1, + ) + }, + err: nil, + }, + { + name: "config specifies more memory than available", + cfg: &Finch{ + CPUs: pointer.Int(4), + Memory: pointer.String("4GiB"), + }, + mockSvc: func(l *mocks.Logger, deps *mocks.LoadSystemDeps, mem *mocks.Memory) { + deps.EXPECT().NumCPU().Return(8) + // 1,074,000,000 == 1GiB + mem.EXPECT().TotalMemory().Return(uint64(1_074_000_000)) + l.EXPECT().Infof( + "The specified amount of memory (%s) is greater than the memory available on this system (%s),\n"+ + "which may lead to severe performance degradation", + "4GiB", + "1GiB", + ) + }, + err: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + deps := mocks.NewLoadSystemDeps(ctrl) + mem := mocks.NewMemory(ctrl) + l := mocks.NewLogger(ctrl) + + tc.mockSvc(l, deps, mem) + + got := validate(tc.cfg, l, deps, mem) + require.Equal(t, tc.err, got) + }) + } +} diff --git a/pkg/dependency/dependency.go b/pkg/dependency/dependency.go new file mode 100644 index 000000000..effdeddf3 --- /dev/null +++ b/pkg/dependency/dependency.go @@ -0,0 +1,89 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package dependency allows for the creation of Dependencies and Dependency Groups, +// and provides functions to install these Groups. +package dependency + +import ( + "fmt" + + "github.com/runfinch/finch/pkg/flog" +) + +// Dependency is a dependency of the Finch binary. It can be a binary, a package, or a file, etc. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_dependency_dependency.go -package=mocks -mock_names Dependency=Dependency . Dependency +type Dependency interface { + RequiresRoot() bool + Installed() bool + Install() error +} + +// Group contains logically related dependencies. +type Group struct { + deps []Dependency + desc string + errMsg string +} + +// NewGroup views deps as ordered dependencies, +// and the returned Group will only install them in the order specified here. +func NewGroup(deps []Dependency, desc string, errMsg string) *Group { + return &Group{ + deps: deps, + desc: desc, + errMsg: errMsg, + } +} + +// installOptional sequentially installs all dependencies in a Group, collecting all +// errors and returning one combined error. +func (g *Group) installOptional(logger flog.Logger) error { + var errs []error + loggedMessage := false + for _, dep := range g.deps { + if dep.Installed() { + continue + } + + if dep.RequiresRoot() && !loggedMessage { + loggedMessage = true + logger.Infoln(g.desc) + } + err := dep.Install() + if err != nil { + // TODO: switch to https://github.com/uber-go/multierr + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return fmt.Errorf("%s: %v", g.errMsg, errs) + } + + return nil +} + +// InstallOptionalDeps installs the supplied dependency groups. +// +// Since the groups are all optional, +// InstallOptionalDeps continues to install the remaining groups even if a group fails to be installed. +// It returns a non-nil error if any of the groups fails to be installed. +func InstallOptionalDeps(groups []*Group, logger flog.Logger) error { + var errs []error + + for _, group := range groups { + err := group.installOptional(logger) + if err != nil { + // TODO: switch to https://github.com/uber-go/multierr + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return fmt.Errorf("failed to install dependencies: %v", errs) + } + + return nil +} diff --git a/pkg/dependency/dependency_test.go b/pkg/dependency/dependency_test.go new file mode 100644 index 000000000..058e64c55 --- /dev/null +++ b/pkg/dependency/dependency_test.go @@ -0,0 +1,209 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package dependency + +import ( + "errors" + "fmt" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestGroup_instalOptional(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + group func(*gomock.Controller) *Group + mockSvc func(*mocks.Logger) + want error + }{ + { + name: "at least one dependency is not installed", + group: func(ctrl *gomock.Controller) *Group { + dep1 := mocks.NewDependency(ctrl) + dep2 := mocks.NewDependency(ctrl) + dep3 := mocks.NewDependency(ctrl) + + group := NewGroup([]Dependency{dep1, dep2, dep3}, "description", "") + + dep1.EXPECT().Installed().Return(true) + dep2.EXPECT().Installed().Return(true) + dep3.EXPECT().Installed().Return(false) + + dep3.EXPECT().Install().Return(nil) + dep3.EXPECT().RequiresRoot().Return(true) + + return group + }, + mockSvc: func(l *mocks.Logger) { + l.EXPECT().Infoln("description") + }, + want: nil, + }, + { + name: "all of the dependencies are already installed", + group: func(ctrl *gomock.Controller) *Group { + dep1 := mocks.NewDependency(ctrl) + dep2 := mocks.NewDependency(ctrl) + dep3 := mocks.NewDependency(ctrl) + + group := NewGroup([]Dependency{dep1, dep2, dep3}, "description", "") + + dep1.EXPECT().Installed().Return(true) + dep2.EXPECT().Installed().Return(true) + dep3.EXPECT().Installed().Return(true) + + return group + }, + mockSvc: func(l *mocks.Logger) { + }, + want: nil, + }, + { + name: "dependency installation throws an error", + group: func(ctrl *gomock.Controller) *Group { + dep1 := mocks.NewDependency(ctrl) + dep2 := mocks.NewDependency(ctrl) + dep3 := mocks.NewDependency(ctrl) + + group := NewGroup([]Dependency{dep1, dep2, dep3}, "description", "error message") + + dep1.EXPECT().Installed().Return(false) + dep1.EXPECT().RequiresRoot().Return(true) + dep1.EXPECT().Install().Return(errors.New("installation failed")) + + dep2.EXPECT().Installed().Return(true) + + dep3.EXPECT().Installed().Return(true) + + return group + }, + mockSvc: func(l *mocks.Logger) { + l.EXPECT().Infoln("description") + }, + want: fmt.Errorf("%s: %v", "error message", []error{errors.New("installation failed")}), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + l := mocks.NewLogger(ctrl) + + tc.mockSvc(l) + group := tc.group(ctrl) + + got := (group.installOptional(l)) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestInstallOptionalDeps(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + groups func(*gomock.Controller) []*Group + mockSvc func(*mocks.Logger) + want error + }{ + { + name: "at least one dependency is not installed yet", + groups: func(ctrl *gomock.Controller) []*Group { + dep1_1 := mocks.NewDependency(ctrl) + dep1_2 := mocks.NewDependency(ctrl) + dep2_1 := mocks.NewDependency(ctrl) + + group1 := NewGroup([]Dependency{dep1_1, dep1_2}, "dep group 1 description", "dep group 1 error message") + group2 := NewGroup([]Dependency{dep2_1}, "dep group 2 description", "dep group 2 error message") + groups := []*Group{group1, group2} + + dep1_1.EXPECT().Installed().Return(false) + dep1_1.EXPECT().RequiresRoot().Return(true) + dep1_1.EXPECT().Install().Return(nil) + + dep1_2.EXPECT().Installed().Return(true) + dep2_1.EXPECT().Installed().Return(true) + + return groups + }, + mockSvc: func(l *mocks.Logger) { + l.EXPECT().Infoln("dep group 1 description") + }, + want: nil, + }, + { + name: "all dependencies are installed", + groups: func(ctrl *gomock.Controller) []*Group { + dep1_1 := mocks.NewDependency(ctrl) + dep2_1 := mocks.NewDependency(ctrl) + + group1 := NewGroup([]Dependency{dep1_1}, "", "") + group2 := NewGroup([]Dependency{dep2_1}, "", "") + groups := []*Group{group1, group2} + + dep1_1.EXPECT().Installed().Return(true) + dep2_1.EXPECT().Installed().Return(true) + + return groups + }, + mockSvc: func(l *mocks.Logger) { + }, + want: nil, + }, + { + name: "dependency installation throws an error", + groups: func(ctrl *gomock.Controller) []*Group { + dep1_1 := mocks.NewDependency(ctrl) + dep1_2 := mocks.NewDependency(ctrl) + dep2_1 := mocks.NewDependency(ctrl) + + group1 := NewGroup([]Dependency{dep1_1, dep1_2}, "dep group 1 description", "dep group 1 error message") + group2 := NewGroup([]Dependency{dep2_1}, "dep group 2 description", "dep group 2 error message") + groups := []*Group{group1, group2} + + dep1_1.EXPECT().Installed().Return(false) + dep1_1.EXPECT().RequiresRoot().Return(true) + dep1_1.EXPECT().Install().Return(errors.New("installation failed")) + + dep1_2.EXPECT().Installed().Return(true) + + dep2_1.EXPECT().Installed().Return(true) + + return groups + }, + mockSvc: func(l *mocks.Logger) { + l.EXPECT().Infoln("dep group 1 description") + }, + want: fmt.Errorf("failed to install dependencies: %v", + []error{fmt.Errorf("%s: %v", "dep group 1 error message", []error{errors.New("installation failed")})}, + ), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + l := mocks.NewLogger(ctrl) + + tc.mockSvc(l) + groups := tc.groups(ctrl) + + got := InstallOptionalDeps(groups, l) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/dependency/vmnet/binaries.go b/pkg/dependency/vmnet/binaries.go new file mode 100644 index 000000000..62fa02666 --- /dev/null +++ b/pkg/dependency/vmnet/binaries.go @@ -0,0 +1,131 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package vmnet + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/path" + + "github.com/spf13/afero" +) + +type binaries struct { + fp path.Finch + fs afero.Fs + cmdCreator command.Creator + l flog.Logger +} + +var _ dependency.Dependency = (*binaries)(nil) + +func newBinaries(fp path.Finch, fs afero.Fs, cmdCreator command.Creator, l flog.Logger) *binaries { + return &binaries{ + // TODO: consider replacing fp with only the strings that are used instead of the entire type + fp: fp, + fs: fs, + cmdCreator: cmdCreator, + l: l, + } +} + +// installationPath returns the installation path under /opt/ (/opt/ was chosen since /opt/ is owned by root). +func (bin *binaries) installationPath() string { + return "/opt/finch" +} + +// socketVmnetBinPath returns a partial path to the socket_vmnet executable. +func (bin *binaries) socketVmnetBinPath() string { + return "/bin/socket_vmnet" +} + +// installationPathSocketVmnetExe retunrs the full path to correctly installed socket_vmnet executable. +func (bin *binaries) installationPathSocketVmnetExe() string { + return fmt.Sprintf("%s%s", bin.installationPath(), bin.socketVmnetBinPath()) +} + +// Path to the build output of socket_vmnet. Must match path in [Makefile]. +func (bin *binaries) buildArtifactPath() string { + return fmt.Sprintf("%s/dependencies/lima-socket_vmnet", bin.fp) +} + +// buildArtifactSocketVmnetExe returns the full path to socket_vmnet executable in the build output directory. +func (bin *binaries) buildArtifactSocketVmnetExe() string { + return fmt.Sprintf("%s%s", bin.buildArtifactPath(), bin.installationPathSocketVmnetExe()) +} + +// Installed checks if a specific file is installed in the privileged location by comparing file contents from the build +// output and where the file should exist in the privileged location. +func (bin *binaries) Installed() bool { + dirExists, err := afero.DirExists(bin.fs, bin.installationPath()) + if err != nil { + bin.l.Errorf("failed to get status of binaries directory: %w", err) + return false + } + if !dirExists { + bin.l.Infof("binaries directory doesn't exist") + return false + } + buildArtifactFileBytes, err := afero.ReadFile(bin.fs, bin.buildArtifactSocketVmnetExe()) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + bin.l.Infof("dependency socket_vmnet file not found: %w", err) + } else { + bin.l.Errorf("failed to read dependency socket_vmnet file: %w", err) + } + return false + } + installedFileBytes, err := afero.ReadFile(bin.fs, bin.installationPathSocketVmnetExe()) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + bin.l.Infof("installed socket_vmnet file not found: %w", err) + } else { + bin.l.Errorf("failed to read installed socket_vmnet file: %w", err) + } + return false + } + return bytes.Equal(buildArtifactFileBytes, installedFileBytes) +} + +// Install creates the privileged location (`bin.installationPath()`), copies socket_vmnet files from the build output +// directory to said location, and sets the correct permissions. +func (bin *binaries) Install() error { + mkdirCmd := bin.cmdCreator.Create("sudo", "mkdir", "-p", bin.installationPath()) + _, err := mkdirCmd.Output() + if err != nil { + return fmt.Errorf("error creating installation directory %s, err: %w", bin.installationPath(), err) + } + + copyCmd := bin.cmdCreator.Create("sudo", "cp", "-rp", fmt.Sprintf("%s%s", bin.buildArtifactPath(), bin.installationPath()), "/opt") + _, err = copyCmd.Output() + if err != nil { + return fmt.Errorf("error copying files to directory %s, err: %w", bin.installationPath(), err) + } + + chownFinchCmd := bin.cmdCreator.Create("sudo", "chown", "root:wheel", bin.installationPath()) + _, err = chownFinchCmd.Output() + if err != nil { + return fmt.Errorf("error changing owner of directory %s, err: %w", bin.installationPath(), err) + } + + installationPathBinDir := fmt.Sprintf("%s/bin", bin.installationPath()) + chownBinCmd := bin.cmdCreator.Create("sudo", "chown", "-R", "root:wheel", installationPathBinDir) + _, err = chownBinCmd.Output() + if err != nil { + return fmt.Errorf("error changing owner of files in directory %s, err: %w", installationPathBinDir, err) + } + + return nil +} + +// RequiresRoot returns true because creating writing to `bin.installationPath()` requires root. +func (bin *binaries) RequiresRoot() bool { + return true +} diff --git a/pkg/dependency/vmnet/binaries_test.go b/pkg/dependency/vmnet/binaries_test.go new file mode 100644 index 000000000..9ce58d887 --- /dev/null +++ b/pkg/dependency/vmnet/binaries_test.go @@ -0,0 +1,256 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Ensures that the binaries required for networking are installed in a privileged location. +// More information here: https://github.com/lima-vm/socket_vmnet +package vmnet + +import ( + "errors" + "fmt" + "io/fs" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + "github.com/runfinch/finch/pkg/path" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + mockFinchPathString = "mock_prefix" + mockFinchPath = path.Finch(mockFinchPathString) +) + +func TestBinaries_installationPath(t *testing.T) { + t.Parallel() + + got := newBinaries("", nil, nil, nil).installationPath() + assert.Equal(t, "/opt/finch", got) +} + +func TestBinaries_socketVmnetBinPath(t *testing.T) { + t.Parallel() + + got := newBinaries("", nil, nil, nil).socketVmnetBinPath() + assert.Equal(t, "/bin/socket_vmnet", got) +} + +func TestBinaries_installationPathSocketVmnetExe(t *testing.T) { + t.Parallel() + + got := newBinaries("", nil, nil, nil).installationPathSocketVmnetExe() + assert.Equal(t, "/opt/finch/bin/socket_vmnet", got) +} + +func TestBinaries_buildArtifactPath(t *testing.T) { + t.Parallel() + + got := newBinaries(mockFinchPath, nil, nil, nil).buildArtifactPath() + assert.Equal(t, "mock_prefix/dependencies/lima-socket_vmnet", got) +} + +func TestBinaries_buildArtifactSocketVmnetExe(t *testing.T) { + t.Parallel() + + got := newBinaries(mockFinchPath, nil, nil, nil).buildArtifactSocketVmnetExe() + assert.Equal(t, "mock_prefix/dependencies/lima-socket_vmnet/opt/finch/bin/socket_vmnet", got) +} + +func TestBinaries_Installed(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(t *testing.T, mFs afero.Fs, l *mocks.Logger) + want bool + }{ + { + name: "happy path", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + fileData := []byte("test data") + err := afero.WriteFile(mFs, "mock_prefix/dependencies/lima-socket_vmnet/opt/finch/bin/socket_vmnet", fileData, 0o666) + require.NoError(t, err) + + require.NoError(t, mFs.MkdirAll("/opt/finch", fs.ModeDir)) + require.NoError(t, afero.WriteFile(mFs, "/opt/finch/bin/socket_vmnet", fileData, 0o666)) + }, + want: true, + }, + { + name: "installation path doesn't exist", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + fileData := []byte("test data") + err := afero.WriteFile(mFs, "mock_prefix/dependencies/lima-socket-vmnet/opt/finch/bin/socket_vmnet", fileData, 0o666) + require.NoError(t, err) + + l.EXPECT().Infof("binaries directory doesn't exist") + }, + want: false, + }, + { + name: "vmnet artifact doesn't exist", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + fileData := []byte("test data") + err := mFs.MkdirAll("/opt/finch", fs.ModeDir) + require.NoError(t, err) + + err = afero.WriteFile(mFs, "/opt/finch/bin/socket_vmnet", fileData, 0o666) + require.NoError(t, err) + + var pathErr fs.PathError + pathErr.Op = "open" + pathErr.Path = "mock_prefix/dependencies/lima-socket_vmnet/opt/finch/bin/socket_vmnet" + pathErr.Err = errors.New("file does not exist") + + l.EXPECT().Infof("dependency socket_vmnet file not found: %w", &pathErr) + }, + want: false, + }, + { + name: "installationPathSocketVmnetExePath path doesn't exist", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + fileData := []byte("test data") + err := afero.WriteFile(mFs, "mock_prefix/dependencies/lima-socket_vmnet/opt/finch/bin/socket_vmnet", fileData, 0o666) + require.NoError(t, err) + + err = mFs.MkdirAll("/opt/finch", fs.ModeDir) + require.NoError(t, err) + + var pathErr fs.PathError + pathErr.Op = "open" + pathErr.Path = "/opt/finch/bin/socket_vmnet" + pathErr.Err = errors.New("file does not exist") + + l.EXPECT().Infof("installed socket_vmnet file not found: %w", &pathErr) + }, + want: false, + }, + { + name: "paths exist, but their contents don't match", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + fileData1 := []byte("test data") + fileData2 := []byte("different test data") + err := afero.WriteFile(mFs, "mock_prefix/dependencies/lima-socket_vmnet/opt/finch/bin/socket_vmnet", fileData1, 0o666) + require.NoError(t, err) + + err = mFs.MkdirAll("/opt/finch", fs.ModeDir) + require.NoError(t, err) + + err = afero.WriteFile(mFs, "/opt/finch/bin/socket_vmnet", fileData2, 0o666) + require.NoError(t, err) + }, + want: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mFs := afero.NewMemMapFs() + l := mocks.NewLogger(ctrl) + tc.mockSvc(t, mFs, l) + + got := newBinaries(mockFinchPath, mFs, nil, l).Installed() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestBinaries_Install(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func( + cmd *mocks.Command, + creator *mocks.CommandCreator) + want error + }{ + { + name: "happy path", + mockSvc: func(cmd *mocks.Command, creator *mocks.CommandCreator) { + cmd.EXPECT().Output().Times(4) + + creator.EXPECT().Create("sudo", "mkdir", "-p", "/opt/finch").Return(cmd) + creator.EXPECT().Create("sudo", "cp", "-rp", "mock_prefix/dependencies/lima-socket_vmnet/opt/finch", "/opt").Return(cmd) + creator.EXPECT().Create("sudo", "chown", "root:wheel", "/opt/finch").Return(cmd) + creator.EXPECT().Create("sudo", "chown", "-R", "root:wheel", "/opt/finch/bin").Return(cmd) + }, + want: nil, + }, + { + name: "sudo mkdir throws an error", + mockSvc: func(cmd *mocks.Command, creator *mocks.CommandCreator) { + cmd.EXPECT().Output().Return([]byte{}, errors.New("mkdir error")) + + creator.EXPECT().Create("sudo", "mkdir", "-p", "/opt/finch").Return(cmd) + }, + want: fmt.Errorf("error creating installation directory %s, err: %w", "/opt/finch", errors.New("mkdir error")), + }, + { + name: "sudo cp throws an error", + mockSvc: func(cmd *mocks.Command, creator *mocks.CommandCreator) { + cmd.EXPECT().Output() + cmd.EXPECT().Output().Return([]byte{}, errors.New("cp error")) + + creator.EXPECT().Create("sudo", "mkdir", "-p", "/opt/finch").Return(cmd) + creator.EXPECT().Create("sudo", "cp", "-rp", "mock_prefix/dependencies/lima-socket_vmnet/opt/finch", "/opt").Return(cmd) + }, + want: fmt.Errorf("error copying files to directory %s, err: %w", "/opt/finch", errors.New("cp error")), + }, + { + name: "sudo chown of the installation directory throws an error", + mockSvc: func(cmd *mocks.Command, creator *mocks.CommandCreator) { + cmd.EXPECT().Output().Times(2) + cmd.EXPECT().Output().Return([]byte{}, errors.New("chown failed")) + + creator.EXPECT().Create("sudo", "mkdir", "-p", "/opt/finch").Return(cmd) + creator.EXPECT().Create("sudo", "cp", "-rp", "mock_prefix/dependencies/lima-socket_vmnet/opt/finch", "/opt").Return(cmd) + creator.EXPECT().Create("sudo", "chown", "root:wheel", "/opt/finch").Return(cmd) + }, + want: fmt.Errorf("error changing owner of directory %s, err: %w", "/opt/finch", errors.New("chown failed")), + }, + { + name: "sudo chown -R of the bin directory throws an error", + mockSvc: func(cmd *mocks.Command, creator *mocks.CommandCreator) { + cmd.EXPECT().Output().Times(3) + cmd.EXPECT().Output().Return([]byte{}, errors.New("chown -R error")) + + creator.EXPECT().Create("sudo", "mkdir", "-p", "/opt/finch").Return(cmd) + creator.EXPECT().Create("sudo", "cp", "-rp", "mock_prefix/dependencies/lima-socket_vmnet/opt/finch", "/opt").Return(cmd) + creator.EXPECT().Create("sudo", "chown", "root:wheel", "/opt/finch").Return(cmd) + creator.EXPECT().Create("sudo", "chown", "-R", "root:wheel", "/opt/finch/bin").Return(cmd) + }, + want: fmt.Errorf("error changing owner of files in directory %s, err: %w", "/opt/finch/bin", errors.New("chown -R error")), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmd := mocks.NewCommand(ctrl) + creator := mocks.NewCommandCreator(ctrl) + tc.mockSvc(cmd, creator) + + got := newBinaries(mockFinchPath, nil, creator, nil).Install() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestBinaries_RequiresRoot(t *testing.T) { + t.Parallel() + + got := newBinaries(mockFinchPath, nil, nil, nil).RequiresRoot() + assert.Equal(t, true, got) +} diff --git a/pkg/dependency/vmnet/sudoers_file.go b/pkg/dependency/vmnet/sudoers_file.go new file mode 100644 index 000000000..3ed736baa --- /dev/null +++ b/pkg/dependency/vmnet/sudoers_file.go @@ -0,0 +1,84 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package vmnet + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + + "github.com/spf13/afero" +) + +type sudoersFile struct { + fs afero.Fs + execCmdCreator command.Creator + limaCmdCreator command.LimaCmdCreator + l flog.Logger +} + +var _ dependency.Dependency = (*sudoersFile)(nil) + +func newSudoersFile(fs afero.Fs, execCmdCreator command.Creator, limaCmdCreator command.LimaCmdCreator, l flog.Logger) *sudoersFile { + return &sudoersFile{ + fs: fs, + execCmdCreator: execCmdCreator, + limaCmdCreator: limaCmdCreator, + l: l, + } +} + +// path returns a path that must match the path in [networks.yaml]. +func (s *sudoersFile) path() string { + return "/etc/sudoers.d/finch-lima" +} + +// Installed checks if any file at `s.path()` matches the output of `lima sudoers`. +func (s *sudoersFile) Installed() bool { + b, err := afero.ReadFile(s.fs, s.path()) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + s.l.Infof("sudoers file not found: %w", err) + } else { + s.l.Errorf("failed to read sudoers file: %w", err) + } + return false + } + cmd := s.limaCmdCreator.CreateWithoutStdio("sudoers") + out, err := cmd.Output() + if err != nil { + s.l.Errorf("failed to run lima sudoers command: %w", err) + return false + } + return bytes.Equal(b, out) +} + +// Install creates the sudoers file at `s.path()` with the contents of `lima sudoers`. +func (s *sudoersFile) Install() error { + sudoers, err := s.limaCmdCreator.CreateWithoutStdio("sudoers").Output() + if err != nil { + return fmt.Errorf("failed to get lima sudoers: %w", err) + } + + cmd := s.execCmdCreator.Create("sudo", "tee", s.path()) + cmd.SetStdin(bytes.NewReader(sudoers)) + // Although we do not care about stdout, we still use Output() instead of Run() + // so that the returned error can be populated with stderr. + _, err = cmd.Output() + if err != nil { + return fmt.Errorf("failed to write to the sudoers file: %w", err) + } + + return nil +} + +// RequiresRoot returns true because creating the new sudoers file at `s.path()` requires root. +func (s *sudoersFile) RequiresRoot() bool { + return true +} diff --git a/pkg/dependency/vmnet/sudoers_file_test.go b/pkg/dependency/vmnet/sudoers_file_test.go new file mode 100644 index 000000000..c4fb1153a --- /dev/null +++ b/pkg/dependency/vmnet/sudoers_file_test.go @@ -0,0 +1,194 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Ensures that output of `lima sudoers` is output to the correct directory. +// This is necessary for networking to work without prompting the user +// for their root password every time the VM is start / stopped. +// More information here: https://github.com/lima-vm/lima/blob/master/docs/network.md#managed-vmnet-networks-192168105024 +package vmnet + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSudoers_path(t *testing.T) { + t.Parallel() + + got := newSudoersFile(nil, nil, nil, nil).path() + assert.Equal(t, "/etc/sudoers.d/finch-lima", got) +} + +func TestSudoers_Installed(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, lc *mocks.LimaCmdCreator, l *mocks.Logger) + want bool + }{ + { + name: "happy path", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, lc *mocks.LimaCmdCreator, l *mocks.Logger) { + sudoersData := []byte("test data") + + err := afero.WriteFile(mFs, "/etc/sudoers.d/finch-lima", sudoersData, 0o666) + require.NoError(t, err) + + lc.EXPECT().CreateWithoutStdio("sudoers").Return(cmd) + cmd.EXPECT().Output().Return(sudoersData, nil) + }, + want: true, + }, + { + name: "sudoers path doesn't exist", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, lc *mocks.LimaCmdCreator, l *mocks.Logger) { + var pathErr fs.PathError + pathErr.Op = "open" + pathErr.Path = "/etc/sudoers.d/finch-lima" + pathErr.Err = errors.New("file does not exist") + + l.EXPECT().Infof("sudoers file not found: %w", &pathErr) + }, + want: false, + }, + { + name: "sudoers command throws an error", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, lc *mocks.LimaCmdCreator, l *mocks.Logger) { + sudoersData := []byte("test data") + wantErr := errors.New("some error") + + err := afero.WriteFile(mFs, "/etc/sudoers.d/finch-lima", sudoersData, 0o666) + require.NoError(t, err) + + lc.EXPECT().CreateWithoutStdio("sudoers").Return(cmd) + cmd.EXPECT().Output().Return([]byte{}, wantErr) + l.EXPECT().Errorf("failed to run lima sudoers command: %w", wantErr) + }, + want: false, + }, + { + name: "paths exist, but contents don't match", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, lc *mocks.LimaCmdCreator, l *mocks.Logger) { + sudoersData1 := []byte("test data") + sudoersData2 := []byte("different test data") + + err := afero.WriteFile(mFs, "/etc/sudoers.d/finch-lima", sudoersData1, 0o666) + require.NoError(t, err) + + lc.EXPECT().CreateWithoutStdio("sudoers").Return(cmd) + cmd.EXPECT().Output().Return(sudoersData2, nil) + }, + want: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmd := mocks.NewCommand(ctrl) + l := mocks.NewLogger(ctrl) + mFs := afero.NewMemMapFs() + mLimaCreator := mocks.NewLimaCmdCreator(ctrl) + tc.mockSvc(t, cmd, mFs, mLimaCreator, l) + + got := newSudoersFile(mFs, nil, mLimaCreator, l).Installed() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestSudoers_Install(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, ec *mocks.CommandCreator, lc *mocks.LimaCmdCreator) + want error + }{ + { + name: "happy path", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, ec *mocks.CommandCreator, lc *mocks.LimaCmdCreator) { + sudoersData := []byte("test data") + mockSudoersOut := []byte("mock_sudoers_out") + + err := afero.WriteFile(mFs, "/etc/sudoers.d/finch-lima", sudoersData, 0o666) + require.NoError(t, err) + + lc.EXPECT().CreateWithoutStdio("sudoers").Return(cmd) + cmd.EXPECT().Output().Return(mockSudoersOut, nil) + ec.EXPECT().Create("sudo", "tee", "/etc/sudoers.d/finch-lima").Return(cmd) + cmd.EXPECT().SetStdin(bytes.NewReader(mockSudoersOut)) + cmd.EXPECT().Output().Return(mockSudoersOut, nil) + }, + want: nil, + }, + { + name: "lima sudoers command throws err", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, ec *mocks.CommandCreator, lc *mocks.LimaCmdCreator) { + sudoersData := []byte("test data") + + err := afero.WriteFile(mFs, "/etc/sudoers.d/finch-lima", sudoersData, 0o666) + require.NoError(t, err) + + lc.EXPECT().CreateWithoutStdio("sudoers").Return(cmd) + cmd.EXPECT().Output().Return(sudoersData, errors.New("sudoers command error")) + }, + want: fmt.Errorf("failed to get lima sudoers: %w", errors.New("sudoers command error")), + }, + { + name: "sudo tee command throws err", + mockSvc: func(t *testing.T, cmd *mocks.Command, mFs afero.Fs, ec *mocks.CommandCreator, lc *mocks.LimaCmdCreator) { + sudoersData := []byte("test data") + mockSudoersOut := []byte("mock_sudoers_out") + + err := afero.WriteFile(mFs, "/etc/sudoers.d/finch-lima", sudoersData, 0o666) + require.NoError(t, err) + + lc.EXPECT().CreateWithoutStdio("sudoers").Return(cmd) + cmd.EXPECT().Output().Return(mockSudoersOut, nil) + ec.EXPECT().Create("sudo", "tee", "/etc/sudoers.d/finch-lima").Return(cmd) + cmd.EXPECT().SetStdin(bytes.NewReader(mockSudoersOut)) + cmd.EXPECT().Output().Return(mockSudoersOut, errors.New("sudo tee command error")) + }, + want: fmt.Errorf("failed to write to the sudoers file: %w", errors.New("sudo tee command error")), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmd := mocks.NewCommand(ctrl) + mFs := afero.NewMemMapFs() + mExecCreator := mocks.NewCommandCreator(ctrl) + mLimaCreator := mocks.NewLimaCmdCreator(ctrl) + tc.mockSvc(t, cmd, mFs, mExecCreator, mLimaCreator) + + got := newSudoersFile(mFs, mExecCreator, mLimaCreator, nil).Install() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestSudoers_RequiresRoot(t *testing.T) { + t.Parallel() + + got := newSudoersFile(nil, nil, nil, nil).RequiresRoot() + assert.Equal(t, true, got) +} diff --git a/pkg/dependency/vmnet/update_override_lima_config.go b/pkg/dependency/vmnet/update_override_lima_config.go new file mode 100644 index 000000000..22b087abd --- /dev/null +++ b/pkg/dependency/vmnet/update_override_lima_config.go @@ -0,0 +1,127 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package vmnet + +import ( + "errors" + "fmt" + "io/fs" + "os" + + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/path" + + "github.com/spf13/afero" + "gopkg.in/yaml.v3" +) + +// overrideLimaConfig updates the lima configuration after other network dependencies are installed. +type overrideLimaConfig struct { + fp path.Finch + binaries dependency.Dependency + sudoersFile dependency.Dependency + fs afero.Fs + l flog.Logger +} + +var _ dependency.Dependency = (*overrideLimaConfig)(nil) + +func newOverrideLimaConfig( + fp path.Finch, + binaries dependency.Dependency, + sudoersFile dependency.Dependency, + fs afero.Fs, + l flog.Logger, +) *overrideLimaConfig { + return &overrideLimaConfig{ + // TODO: consider replacing fp with only the strings that are used instead of the entire type + fp: fp, + binaries: binaries, + sudoersFile: sudoersFile, + fs: fs, + l: l, + } +} + +// Snippet to append to a lima yaml file to setup a managed network called "finch-shared". +// This must match the value in [networks.yaml]. +// TODO: Use limayaml.LimaYAML instead of appending a raw string? +const networkConfigString = `networks: + - lima: finch-shared +` + +// NetworkConfig is a struct for (partially) deserializing lima yaml. +type NetworkConfig struct { + Networks []map[string]string `yaml:"networks"` +} + +// verifyConfigHasNetworkSection deserializes a yaml file at filePath and verifies that it has the expected value. +func (overConf *overrideLimaConfig) verifyConfigHasNetworkSection(filePath string) bool { + yamlFile, err := afero.ReadFile(overConf.fs, filePath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + overConf.l.Debugf("config file not found: %w", err) + } else { + overConf.l.Errorf("failed to read config file: %w", err) + } + return false + } + var cfg NetworkConfig + err = yaml.Unmarshal(yamlFile, &cfg) + if err != nil { + overConf.l.Errorf("failed to unmarshal YAML from override config file: %w", err) + return false + } + + networksLen := len(cfg.Networks) + if networksLen != 1 { + overConf.l.Errorf("override config file has incorrect number of Networks defined (%d)", networksLen) + return false + } + + return cfg.Networks[0]["lima"] == "finch-shared" +} + +// appendNetworkConfiguration adds a new network config section to a file at filePath. +func (overConf *overrideLimaConfig) appendNetworkConfiguration(filePath string) error { + f, err := overConf.fs.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("error opening file at path %s, error: %w", filePath, err) + } + defer func() { + if err := f.Close(); err != nil { + overConf.l.Errorf("error closing file at path %s, error: %v", filePath, err) + } + }() + if _, err := f.WriteString(networkConfigString); err != nil { + return fmt.Errorf("error writing to file at path %s", filePath) + } + + return nil +} + +// shouldAddNetworksConfig returns true iff binaries and sudoers are installed as +// updating the network config without those dependencies leads to a broken user experience. +func (overConf *overrideLimaConfig) shouldAddNetworksConfig() bool { + return overConf.binaries.Installed() && overConf.sudoersFile.Installed() +} + +// Installed returns true iff lima config has been updated. +func (overConf *overrideLimaConfig) Installed() bool { + return overConf.verifyConfigHasNetworkSection(overConf.fp.LimaOverrideConfigPath()) +} + +// Install adds the networks config block to liam's override config yaml file. +// Only adds if the shouldAddNetworksConfig() helper function is true. +func (overConf *overrideLimaConfig) Install() error { + if !overConf.shouldAddNetworksConfig() { + return fmt.Errorf("skipping installation of network configuration because pre-requisites are missing") + } + return overConf.appendNetworkConfiguration(overConf.fp.LimaOverrideConfigPath()) +} + +func (overConf *overrideLimaConfig) RequiresRoot() bool { + return false +} diff --git a/pkg/dependency/vmnet/update_override_lima_config_test.go b/pkg/dependency/vmnet/update_override_lima_config_test.go new file mode 100644 index 000000000..f1bbe2458 --- /dev/null +++ b/pkg/dependency/vmnet/update_override_lima_config_test.go @@ -0,0 +1,280 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package vmnet + +import ( + "errors" + "fmt" + "io/fs" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/runfinch/finch/pkg/mocks" + "github.com/runfinch/finch/pkg/path" +) + +func TestOverrideLimaConfig_verifyConfigHasNetworkSection(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + filePath string + mockSvc func(t *testing.T, mFs afero.Fs, l *mocks.Logger) + want bool + }{ + { + name: "happy path", + filePath: "mock_config_file", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + require.NoError(t, afero.WriteFile(mFs, "mock_config_file", []byte(networkConfigString), 0o644)) + }, + want: true, + }, + { + name: "config file doesn't exist", + filePath: "mock_config_file", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + var pathErr fs.PathError + pathErr.Op = "open" + pathErr.Path = "mock_config_file" + pathErr.Err = errors.New("file does not exist") + + l.EXPECT().Debugf("config file not found: %w", &pathErr) + }, + want: false, + }, + { + name: "config file contains invalid YAML", + filePath: "mock_config_file", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + require.NoError(t, afero.WriteFile(mFs, "mock_config_file", []byte("this isn't YAML"), 0o644)) + + var typeErr yaml.TypeError + typeErr.Errors = []string{"line 1: cannot unmarshal !!str `this is...` into vmnet.NetworkConfig"} + + l.EXPECT().Errorf("failed to unmarshal YAML from override config file: %w", &typeErr) + }, + want: false, + }, + { + name: "config file contains more than one network section", + filePath: "mock_config_file", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + data := `networks: +- lima: finch-shared +- not-lima: not-finch-shared +` + require.NoError(t, afero.WriteFile(mFs, "mock_config_file", []byte(data), 0o644)) + + l.EXPECT().Errorf("override config file has incorrect number of Networks defined (%d)", 2) + }, + want: false, + }, + { + name: "config file contains invalid network section", + filePath: "mock_config_file", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + data := strings.ReplaceAll(networkConfigString, "finch-shared", "not-finch-shared") + require.NoError(t, afero.WriteFile(mFs, "mock_config_file", []byte(data), 0o644)) + }, + want: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + l := mocks.NewLogger(ctrl) + mFs := afero.NewMemMapFs() + tc.mockSvc(t, mFs, l) + + got := newOverrideLimaConfig("", nil, nil, mFs, l).verifyConfigHasNetworkSection(tc.filePath) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestOverrideLimaConfig_appendNetworkConfiguration(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + filePath string + mockSvc func(mFs afero.Fs) + want error + postRunCheck func(*testing.T, afero.Fs) + }{ + { + name: "happy path", + filePath: "mock_config_file", + mockSvc: func(mFs afero.Fs) {}, + want: nil, + postRunCheck: func(t *testing.T, mFs afero.Fs) { + fileBytes, err := afero.ReadFile(mFs, "mock_config_file") + require.NoError(t, err) + assert.Equal(t, []byte(networkConfigString), fileBytes) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mFs := afero.NewMemMapFs() + tc.mockSvc(mFs) + + got := newOverrideLimaConfig("", nil, nil, mFs, nil).appendNetworkConfiguration(tc.filePath) + require.Equal(t, tc.want, got) + tc.postRunCheck(t, mFs) + }) + } +} + +func TestOverrideLimaConfig_shouldAddNetworksConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.Dependency, *mocks.Dependency) + want bool + }{ + { + name: "happy path", + mockSvc: func(b *mocks.Dependency, s *mocks.Dependency) { + b.EXPECT().Installed().Return(true) + s.EXPECT().Installed().Return(true) + }, + want: true, + }, + { + name: "binaries are not installed", + mockSvc: func(b *mocks.Dependency, s *mocks.Dependency) { + b.EXPECT().Installed().Return(false) + }, + want: false, + }, + { + name: "sudoers file is not installed", + mockSvc: func(b *mocks.Dependency, s *mocks.Dependency) { + b.EXPECT().Installed().Return(true) + s.EXPECT().Installed().Return(false) + }, + want: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + b := mocks.NewDependency(ctrl) + s := mocks.NewDependency(ctrl) + tc.mockSvc(b, s) + + got := newOverrideLimaConfig(mockFinchPath, b, s, nil, nil).shouldAddNetworksConfig() + assert.Equal(t, got, tc.want) + }) + } +} + +func TestOverrideLimaConfig_Installed(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(t *testing.T, mFs afero.Fs, fp path.Finch) + want bool + }{ + { + name: "happy path", + mockSvc: func(t *testing.T, mFs afero.Fs, fp path.Finch) { + file, err := mFs.Create(fp.LimaOverrideConfigPath()) + require.NoError(t, err) + + _, err = file.WriteString(networkConfigString) + require.NoError(t, err) + }, + want: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mFs := afero.NewMemMapFs() + tc.mockSvc(t, mFs, mockFinchPath) + + got := newOverrideLimaConfig(mockFinchPath, nil, nil, mFs, nil).Installed() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestOverrideLimaConfig_Install(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*testing.T, *mocks.Dependency, *mocks.Dependency, afero.Fs, path.Finch) + want error + }{ + { + name: "happy path", + mockSvc: func(t *testing.T, b *mocks.Dependency, s *mocks.Dependency, mFs afero.Fs, fp path.Finch) { + b.EXPECT().Installed().Return(true) + s.EXPECT().Installed().Return(true) + + _, err := mFs.Create(fp.LimaOverrideConfigPath()) + require.NoError(t, err) + }, + want: nil, + }, + { + name: "shouldAddNetwork is false", + mockSvc: func(t *testing.T, b *mocks.Dependency, s *mocks.Dependency, mFs afero.Fs, fp path.Finch) { + b.EXPECT().Installed().Return(false) + }, + want: fmt.Errorf("skipping installation of network configuration because pre-requisites are missing"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + b := mocks.NewDependency(ctrl) + s := mocks.NewDependency(ctrl) + mFs := afero.NewMemMapFs() + tc.mockSvc(t, b, s, mFs, mockFinchPath) + + overrideLimaConfig := newOverrideLimaConfig(mockFinchPath, b, s, mFs, nil) + got := overrideLimaConfig.Install() + assert.Equal(t, got, tc.want) + }) + } +} + +func TestOverrideLimaConfig_RequiresRoot(t *testing.T) { + t.Parallel() + + got := newOverrideLimaConfig("", nil, nil, nil, nil).RequiresRoot() + assert.Equal(t, false, got) +} diff --git a/pkg/dependency/vmnet/vmnet.go b/pkg/dependency/vmnet/vmnet.go new file mode 100644 index 000000000..55a96b338 --- /dev/null +++ b/pkg/dependency/vmnet/vmnet.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package vmnet handles installation and configuration of dependencies needed for Lima's managed networking +// and port-forwarding to work, with minimal user interaction. +// +// For more information, see [Lima Managed VMNet]. +// +// [Lima Managed VMNet]: https://github.com/lima-vm/lima/blob/master/docs/network.md#managed-vmnet-networks-192168105024 +package vmnet + +import ( + "github.com/spf13/afero" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/dependency" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/path" +) + +const ( + description = "Requesting root access to finish network dependency configuration" + errMsg = "Failed to finish installing rootful dependencies" + + " which are needed for external network access within the guest OS." + + " Boot will continue, but container exposed ports will not be accessible from macOS." +) + +// NewDependencyGroup returns a dependency group that contains all the dependencies required to make vmnet networking work. +func NewDependencyGroup( + execCmdCreator command.Creator, + limaCmdCreator command.LimaCmdCreator, + fs afero.Fs, + fp path.Finch, + logger flog.Logger, +) *dependency.Group { + deps := newDeps(execCmdCreator, limaCmdCreator, fs, fp, logger) + return dependency.NewGroup(deps, description, errMsg) +} + +func newDeps( + execCmdCreator command.Creator, + limaCmdCreator command.LimaCmdCreator, + fs afero.Fs, + fp path.Finch, + logger flog.Logger, +) []dependency.Dependency { + binaries := newBinaries(fp, fs, execCmdCreator, logger) + sudoersFile := newSudoersFile(fs, execCmdCreator, limaCmdCreator, logger) + overrideLimaConfig := newOverrideLimaConfig(fp, binaries, sudoersFile, fs, logger) + + // Ordering of these dependencies is important because overrideLimaConfig has a dependency on binaries and sudoersFile. + // Adding the network configuration to Lima's overrideConfig without first installing binaries and sudoers leads + // to a broken user experience. + // Since Group.Install() installs dependencies serially, in-order, and continues to the next dependency after an error, + // overrideLimaConfig itself checks to make sure that binaries and sudoers are installed before installing itself. + return []dependency.Dependency{binaries, sudoersFile, overrideLimaConfig} +} diff --git a/pkg/dependency/vmnet/vmnet_test.go b/pkg/dependency/vmnet/vmnet_test.go new file mode 100644 index 000000000..c30e0844a --- /dev/null +++ b/pkg/dependency/vmnet/vmnet_test.go @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package vmnet + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/runfinch/finch/pkg/dependency" +) + +func Test_NewDependencyGroup(t *testing.T) { + t.Parallel() + + want := dependency.NewGroup(newDeps(nil, nil, nil, "", nil), description, errMsg) + got := NewDependencyGroup(nil, nil, nil, "", nil) + assert.Equal(t, want, got) +} + +func Test_newDeps(t *testing.T) { + t.Parallel() + + got := newDeps(nil, nil, nil, "", nil) + require.Equal(t, 3, len(got)) + assert.IsType(t, (*binaries)(nil), got[0]) + assert.IsType(t, (*sudoersFile)(nil), got[1]) + assert.IsType(t, (*overrideLimaConfig)(nil), got[2]) +} diff --git a/pkg/flog/level_string.go b/pkg/flog/level_string.go new file mode 100644 index 000000000..828c5024b --- /dev/null +++ b/pkg/flog/level_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=Level"; DO NOT EDIT. + +package flog + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Debug-0] + _ = x[Panic-1] +} + +const _Level_name = "DebugPanic" + +var _Level_index = [...]uint8{0, 5, 10} + +func (i Level) String() string { + if i < 0 || i >= Level(len(_Level_index)-1) { + return "Level(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Level_name[_Level_index[i]:_Level_index[i+1]] +} diff --git a/pkg/flog/log.go b/pkg/flog/log.go new file mode 100644 index 000000000..fdb09d2cd --- /dev/null +++ b/pkg/flog/log.go @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package flog contains logging-related APIs. +package flog + +// Logger should be used to write any logs. No concrete implementations should be directly used. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/logger.go -package=mocks -mock_names Logger=Logger . Logger +type Logger interface { + Debugf(format string, args ...interface{}) + Debugln(args ...interface{}) + Info(args ...interface{}) + Infof(format string, args ...interface{}) + Infoln(args ...interface{}) + Warnln(args ...interface{}) + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fatal(args ...interface{}) + SetLevel(level Level) +} + +// Level denotes a log level. Check the constants below for more information. +type Level int + +//go:generate stringer -type=Level +const ( + Debug Level = iota + Panic +) diff --git a/pkg/flog/logrus.go b/pkg/flog/logrus.go new file mode 100644 index 000000000..4cb8d4a14 --- /dev/null +++ b/pkg/flog/logrus.go @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package flog + +import "github.com/sirupsen/logrus" + +// Logrus implements the Logger interface. +type Logrus struct{} + +var _ Logger = (*Logrus)(nil) + +// NewLogrus returns a new logrus logger. +func NewLogrus() *Logrus { + return &Logrus{} +} + +// Debugf logs a message at level Debug. +func (l *Logrus) Debugf(format string, args ...interface{}) { + logrus.Debugf(format, args...) +} + +// Debugln logs a message at level Debug. +func (l *Logrus) Debugln(args ...interface{}) { + logrus.Debugln(args...) +} + +// Info logs a message at level Info. +func (l *Logrus) Info(args ...interface{}) { + logrus.Info(args...) +} + +// Infof logs a message at level Info. +func (l *Logrus) Infof(format string, args ...interface{}) { + logrus.Infof(format, args...) +} + +// Infoln logs a message at level Info. +func (l *Logrus) Infoln(args ...interface{}) { + logrus.Infoln(args...) +} + +// Warnln logs a message at level Warn. +func (l *Logrus) Warnln(args ...interface{}) { + logrus.Warnln(args...) +} + +// Error logs a message at level Error. +func (l *Logrus) Error(args ...interface{}) { + logrus.Error(args...) +} + +// Errorf logs a message at level Error. +func (l *Logrus) Errorf(format string, args ...interface{}) { + logrus.Errorf(format, args...) +} + +// Fatal logs a message at level Fatal. +func (l *Logrus) Fatal(args ...interface{}) { + logrus.Fatal(args...) +} + +// SetLevel sets the level of the logger. +func (l *Logrus) SetLevel(level Level) { + switch level { + case Debug: + logrus.SetLevel(logrus.DebugLevel) + case Panic: + logrus.SetLevel(logrus.PanicLevel) + } +} diff --git a/pkg/fmemory/fmemory.go b/pkg/fmemory/fmemory.go new file mode 100644 index 000000000..57726a7d9 --- /dev/null +++ b/pkg/fmemory/fmemory.go @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package fmemory provides functions and methods to get memory information about the current system. +package fmemory + +import ( + "github.com/pbnjay/memory" +) + +// Memory abstracts out memory.TotalMemory to facilitate unit testing. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_fmemory_memory.go -package=mocks -mock_names Memory=Memory . Memory +type Memory interface { + TotalMemory() uint64 +} + +type mem struct{} + +func (mem) TotalMemory() uint64 { + return memory.TotalMemory() +} + +// NewMemory returns a Memory instance that calls memory.TotalMemory under the hood. +func NewMemory() Memory { + return &mem{} +} diff --git a/pkg/fssh/fssh.go b/pkg/fssh/fssh.go new file mode 100644 index 000000000..04fb8cf09 --- /dev/null +++ b/pkg/fssh/fssh.go @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package fssh provides functions and methods to configure and create SSH connections. +package fssh + +import ( + "errors" + "fmt" + "net" + + "github.com/spf13/afero" + "golang.org/x/crypto/ssh" +) + +// Dialer abstracts out ssh.Dial to facilitate unit testing. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/pkg_ssh_dialer.go -package=mocks -mock_names Dialer=Dialer . Dialer +type Dialer interface { + Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) +} + +var _ Dialer = (*dialer)(nil) + +type dialer struct{} + +func (*dialer) Dial(network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { + return ssh.Dial(network, addr, config) +} + +// NewDialer returns a Dialer that calls ssh.Dial under the hood. +func NewDialer() Dialer { + return &dialer{} +} + +func hostKeyCallback() ssh.HostKeyCallback { + return func(_ string, remote net.Addr, _ ssh.PublicKey) error { + addr, ok := remote.(*net.TCPAddr) + if !ok { + return errors.New("failed to convert the remote address to a TCP address") + } + if !addr.IP.IsLoopback() { + return fmt.Errorf("addresses that are not loopback addresses are not supported, address: %s", addr.String()) + } + return nil + } +} + +// NewClientConfig returns a client config that can only connect to a loopback address. +func NewClientConfig(fs afero.Fs, user string, privateKeyPath string) (*ssh.ClientConfig, error) { + fileBytes, err := afero.ReadFile(fs, privateKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to open private key file: %w", err) + } + signer, err := ssh.ParsePrivateKey(fileBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key from %s: %w", privateKeyPath, err) + } + + auths := []ssh.AuthMethod{ssh.PublicKeys(signer)} + + return &ssh.ClientConfig{ + User: user, + Auth: auths, + HostKeyCallback: hostKeyCallback(), + }, nil +} diff --git a/pkg/fssh/fssh_test.go b/pkg/fssh/fssh_test.go new file mode 100644 index 000000000..647eb2844 --- /dev/null +++ b/pkg/fssh/fssh_test.go @@ -0,0 +1,141 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package fssh + +import ( + "errors" + "fmt" + "io/fs" + "net" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func Test_hostKeyCallback(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + remote net.Addr + want error + }{ + { + name: "happy path, remote is a valid TCP loopback address", + remote: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}, + want: nil, + }, + { + name: "remote is not a valid address", + remote: nil, + want: errors.New("failed to convert the remote address to a TCP address"), + }, + { + name: "remote is not a loopback address", + remote: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 8080}, + want: errors.New("addresses that are not loopback addresses are not supported, address: 192.0.2.1:8080"), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := hostKeyCallback()("", tc.remote, nil) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestNewClientConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + user string + privateKeyPath string + mockSvc func(t *testing.T, fs afero.Fs) + want *ssh.ClientConfig + wantErr error + }{ + { + name: "happy path", + user: "test", + privateKeyPath: "/private_key", + mockSvc: func(t *testing.T, fs afero.Fs) { + err := afero.WriteFile(fs, "/private_key", []byte(` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAfR367TtAGV+abvj4pRDcFdU2enKE+iC4qF3LNJF9eyQAAAKjEIxhXxCMY +VwAAAAtzc2gtZWQyNTUxOQAAACAfR367TtAGV+abvj4pRDcFdU2enKE+iC4qF3LNJF9eyQ +AAAEANzWA32dcyDqkfg7zbzt7D76PTyyaX0n1/goKJNPLYyB9HfrtO0AZX5pu+PilENwV1 +TZ6coT6ILioXcs0kX17JAAAAI2FsdmFqdXNAODg2NjVhMGJmN2NhLmFudC5hbWF6b24uY2 +9tAQI= +-----END OPENSSH PRIVATE KEY-----`), 0o644) + require.NoError(t, err) + }, + want: &ssh.ClientConfig{ + User: "test", + }, + wantErr: nil, + }, + { + name: "private key file doesn't exist", + user: "test", + privateKeyPath: "/private_key", + mockSvc: func(t *testing.T, fs afero.Fs) {}, + want: nil, + wantErr: fmt.Errorf( + "failed to open private key file: %w", + &fs.PathError{Op: "open", Path: "/private_key", Err: errors.New("file does not exist")}, + ), + }, + { + name: "invalid private key file contents", + user: "test", + privateKeyPath: "/private_key", + mockSvc: func(t *testing.T, fs afero.Fs) { + err := afero.WriteFile(fs, "/private_key", []byte(`not a private key`), 0o644) + require.NoError(t, err) + }, + want: nil, + wantErr: fmt.Errorf("failed to parse private key from %s: %w", "/private_key", fmt.Errorf("ssh: no key found")), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + + tc.mockSvc(t, fs) + got, gotErr := NewClientConfig(fs, tc.user, tc.privateKeyPath) + require.Equal(t, tc.wantErr, gotErr) + + if gotErr == nil { + require.Len(t, got.Auth, 1) + // Comparing functions is always false unless both are nil. + // See https://github.com/stretchr/testify/issues/182. + assert.NotNil(t, got.Auth[0]) + assert.NotNil(t, got.HostKeyCallback) + + // Assert all of the non-function members of the struct. + assert.Equal(t, tc.want.User, got.User) + assert.Equal(t, tc.want.RekeyThreshold, got.RekeyThreshold) + assert.Equal(t, tc.want.KeyExchanges, got.KeyExchanges) + assert.Equal(t, tc.want.Ciphers, got.Ciphers) + assert.Equal(t, tc.want.MACs, got.MACs) + assert.Equal(t, tc.want.ClientVersion, got.ClientVersion) + assert.Equal(t, tc.want.HostKeyAlgorithms, got.HostKeyAlgorithms) + assert.Equal(t, tc.want.Timeout, got.Timeout) + } + }) + } +} diff --git a/pkg/lima/lima.go b/pkg/lima/lima.go new file mode 100644 index 000000000..73d248e4e --- /dev/null +++ b/pkg/lima/lima.go @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package lima provides common methods related to Lima. +package lima + +import ( + "errors" + "strings" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/flog" +) + +// VMStatus for Lima. +// Relevant status defined in Lima upstream: +// https://github.com/lima-vm/lima/blob/fc783ec455a91d70639f9a1d7f22e9890fe6b1cd/pkg/store/instance.go#L23. +type VMStatus int64 + +// Finch CLI assumes there are only 4 VM status below. Adding more statuses will need to make changes in the caller side. +const ( + Running VMStatus = iota + Stopped + Nonexistent + Unknown +) + +// GetVMStatus returns the Lima VM status. +func GetVMStatus(creator command.LimaCmdCreator, logger flog.Logger, instanceName string) (VMStatus, error) { + args := []string{"ls", "-f", "{{.Status}}", instanceName} + cmd := creator.CreateWithoutStdio(args...) + out, err := cmd.Output() + if err != nil { + return Unknown, err + } + status := strings.TrimSpace(string(out)) + return toVMStatus(status, logger) +} + +func toVMStatus(status string, logger flog.Logger) (VMStatus, error) { + logger.Debugf("Status of virtual machine: %s", status) + switch status { + case "": + return Nonexistent, nil + case "Running": + return Running, nil + case "Stopped": + return Stopped, nil + default: + return Unknown, errors.New("unrecognized system status") + } +} diff --git a/pkg/lima/lima_test.go b/pkg/lima/lima_test.go new file mode 100644 index 000000000..b38be6bf1 --- /dev/null +++ b/pkg/lima/lima_test.go @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package lima_test + +import ( + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/runfinch/finch/pkg/lima" + "github.com/runfinch/finch/pkg/mocks" +) + +func TestGetVMStatus(t *testing.T) { + t.Parallel() + + instanceName := "finch" + mockArgs := []string{"ls", "-f", "{{.Status}}", instanceName} + testCases := []struct { + name string + want lima.VMStatus + wantErr error + mockSvc func(*mocks.LimaCmdCreator, *mocks.Logger, *mocks.Command) + }{ + { + name: "running VM", + want: lima.Running, + wantErr: nil, + mockSvc: func(creator *mocks.LimaCmdCreator, logger *mocks.Logger, cmd *mocks.Command) { + creator.EXPECT().CreateWithoutStdio(mockArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte("Running "), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + }, + }, + { + name: "stopped VM", + want: lima.Stopped, + wantErr: nil, + mockSvc: func(creator *mocks.LimaCmdCreator, logger *mocks.Logger, cmd *mocks.Command) { + creator.EXPECT().CreateWithoutStdio(mockArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte("Stopped "), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + }, + }, + { + name: "nonexistent VM", + want: lima.Nonexistent, + wantErr: nil, + mockSvc: func(creator *mocks.LimaCmdCreator, logger *mocks.Logger, cmd *mocks.Command) { + creator.EXPECT().CreateWithoutStdio(mockArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte(" "), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + { + name: "unknown VM status", + want: lima.Unknown, + wantErr: errors.New("unrecognized system status"), + mockSvc: func(creator *mocks.LimaCmdCreator, logger *mocks.Logger, cmd *mocks.Command) { + creator.EXPECT().CreateWithoutStdio(mockArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte("Broken "), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + want: lima.Unknown, + wantErr: errors.New("get status error"), + mockSvc: func(creator *mocks.LimaCmdCreator, logger *mocks.Logger, cmd *mocks.Command) { + creator.EXPECT().CreateWithoutStdio(mockArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte("Broken "), errors.New("get status error")) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + creator := mocks.NewLimaCmdCreator(ctrl) + statusCmd := mocks.NewCommand(ctrl) + logger := mocks.NewLogger(ctrl) + tc.mockSvc(creator, logger, statusCmd) + got, err := lima.GetVMStatus(creator, logger, instanceName) + assert.Equal(t, tc.wantErr, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/mocks/command_command.go b/pkg/mocks/command_command.go new file mode 100644 index 000000000..e1d621e7b --- /dev/null +++ b/pkg/mocks/command_command.go @@ -0,0 +1,130 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/command (interfaces: Command) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + io "io" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// Command is a mock of Command interface. +type Command struct { + ctrl *gomock.Controller + recorder *CommandMockRecorder +} + +// CommandMockRecorder is the mock recorder for Command. +type CommandMockRecorder struct { + mock *Command +} + +// NewCommand creates a new mock instance. +func NewCommand(ctrl *gomock.Controller) *Command { + mock := &Command{ctrl: ctrl} + mock.recorder = &CommandMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Command) EXPECT() *CommandMockRecorder { + return m.recorder +} + +// CombinedOutput mocks base method. +func (m *Command) CombinedOutput() ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CombinedOutput") + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CombinedOutput indicates an expected call of CombinedOutput. +func (mr *CommandMockRecorder) CombinedOutput() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*Command)(nil).CombinedOutput)) +} + +// Output mocks base method. +func (m *Command) Output() ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Output") + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Output indicates an expected call of Output. +func (mr *CommandMockRecorder) Output() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Output", reflect.TypeOf((*Command)(nil).Output)) +} + +// Run mocks base method. +func (m *Command) Run() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Run") + ret0, _ := ret[0].(error) + return ret0 +} + +// Run indicates an expected call of Run. +func (mr *CommandMockRecorder) Run() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*Command)(nil).Run)) +} + +// SetEnv mocks base method. +func (m *Command) SetEnv(arg0 []string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetEnv", arg0) +} + +// SetEnv indicates an expected call of SetEnv. +func (mr *CommandMockRecorder) SetEnv(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEnv", reflect.TypeOf((*Command)(nil).SetEnv), arg0) +} + +// SetStderr mocks base method. +func (m *Command) SetStderr(arg0 io.Writer) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStderr", arg0) +} + +// SetStderr indicates an expected call of SetStderr. +func (mr *CommandMockRecorder) SetStderr(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStderr", reflect.TypeOf((*Command)(nil).SetStderr), arg0) +} + +// SetStdin mocks base method. +func (m *Command) SetStdin(arg0 io.Reader) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStdin", arg0) +} + +// SetStdin indicates an expected call of SetStdin. +func (mr *CommandMockRecorder) SetStdin(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStdin", reflect.TypeOf((*Command)(nil).SetStdin), arg0) +} + +// SetStdout mocks base method. +func (m *Command) SetStdout(arg0 io.Writer) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetStdout", arg0) +} + +// SetStdout indicates an expected call of SetStdout. +func (mr *CommandMockRecorder) SetStdout(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStdout", reflect.TypeOf((*Command)(nil).SetStdout), arg0) +} diff --git a/pkg/mocks/command_command_creator.go b/pkg/mocks/command_command_creator.go new file mode 100644 index 000000000..c77c976ef --- /dev/null +++ b/pkg/mocks/command_command_creator.go @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/command (interfaces: Creator) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + command "github.com/runfinch/finch/pkg/command" +) + +// CommandCreator is a mock of Creator interface. +type CommandCreator struct { + ctrl *gomock.Controller + recorder *CommandCreatorMockRecorder +} + +// CommandCreatorMockRecorder is the mock recorder for CommandCreator. +type CommandCreatorMockRecorder struct { + mock *CommandCreator +} + +// NewCommandCreator creates a new mock instance. +func NewCommandCreator(ctrl *gomock.Controller) *CommandCreator { + mock := &CommandCreator{ctrl: ctrl} + mock.recorder = &CommandCreatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *CommandCreator) EXPECT() *CommandCreatorMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *CommandCreator) Create(arg0 string, arg1 ...string) command.Command { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(command.Command) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *CommandCreatorMockRecorder) Create(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*CommandCreator)(nil).Create), varargs...) +} diff --git a/pkg/mocks/command_lima_cmd_creator.go b/pkg/mocks/command_lima_cmd_creator.go new file mode 100644 index 000000000..03862f25a --- /dev/null +++ b/pkg/mocks/command_lima_cmd_creator.go @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/command (interfaces: LimaCmdCreator) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + command "github.com/runfinch/finch/pkg/command" +) + +// LimaCmdCreator is a mock of LimaCmdCreator interface. +type LimaCmdCreator struct { + ctrl *gomock.Controller + recorder *LimaCmdCreatorMockRecorder +} + +// LimaCmdCreatorMockRecorder is the mock recorder for LimaCmdCreator. +type LimaCmdCreatorMockRecorder struct { + mock *LimaCmdCreator +} + +// NewLimaCmdCreator creates a new mock instance. +func NewLimaCmdCreator(ctrl *gomock.Controller) *LimaCmdCreator { + mock := &LimaCmdCreator{ctrl: ctrl} + mock.recorder = &LimaCmdCreatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *LimaCmdCreator) EXPECT() *LimaCmdCreatorMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *LimaCmdCreator) Create(arg0 ...string) command.Command { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(command.Command) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *LimaCmdCreatorMockRecorder) Create(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*LimaCmdCreator)(nil).Create), arg0...) +} + +// CreateWithoutStdio mocks base method. +func (m *LimaCmdCreator) CreateWithoutStdio(arg0 ...string) command.Command { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateWithoutStdio", varargs...) + ret0, _ := ret[0].(command.Command) + return ret0 +} + +// CreateWithoutStdio indicates an expected call of CreateWithoutStdio. +func (mr *LimaCmdCreatorMockRecorder) CreateWithoutStdio(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWithoutStdio", reflect.TypeOf((*LimaCmdCreator)(nil).CreateWithoutStdio), arg0...) +} + +// RunWithReplacingStdout mocks base method. +func (m *LimaCmdCreator) RunWithReplacingStdout(arg0 []command.Replacement, arg1 ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RunWithReplacingStdout", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// RunWithReplacingStdout indicates an expected call of RunWithReplacingStdout. +func (mr *LimaCmdCreatorMockRecorder) RunWithReplacingStdout(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunWithReplacingStdout", reflect.TypeOf((*LimaCmdCreator)(nil).RunWithReplacingStdout), varargs...) +} diff --git a/pkg/mocks/finch_finder_deps.go b/pkg/mocks/finch_finder_deps.go new file mode 100644 index 000000000..545cdff6d --- /dev/null +++ b/pkg/mocks/finch_finder_deps.go @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/path (interfaces: FinchFinderDeps) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// FinchFinderDeps is a mock of FinchFinderDeps interface. +type FinchFinderDeps struct { + ctrl *gomock.Controller + recorder *FinchFinderDepsMockRecorder +} + +// FinchFinderDepsMockRecorder is the mock recorder for FinchFinderDeps. +type FinchFinderDepsMockRecorder struct { + mock *FinchFinderDeps +} + +// NewFinchFinderDeps creates a new mock instance. +func NewFinchFinderDeps(ctrl *gomock.Controller) *FinchFinderDeps { + mock := &FinchFinderDeps{ctrl: ctrl} + mock.recorder = &FinchFinderDepsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *FinchFinderDeps) EXPECT() *FinchFinderDepsMockRecorder { + return m.recorder +} + +// Env mocks base method. +func (m *FinchFinderDeps) Env(arg0 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Env", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// Env indicates an expected call of Env. +func (mr *FinchFinderDepsMockRecorder) Env(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Env", reflect.TypeOf((*FinchFinderDeps)(nil).Env), arg0) +} + +// EvalSymlinks mocks base method. +func (m *FinchFinderDeps) EvalSymlinks(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EvalSymlinks", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EvalSymlinks indicates an expected call of EvalSymlinks. +func (mr *FinchFinderDepsMockRecorder) EvalSymlinks(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalSymlinks", reflect.TypeOf((*FinchFinderDeps)(nil).EvalSymlinks), arg0) +} + +// Executable mocks base method. +func (m *FinchFinderDeps) Executable() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Executable") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Executable indicates an expected call of Executable. +func (mr *FinchFinderDepsMockRecorder) Executable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Executable", reflect.TypeOf((*FinchFinderDeps)(nil).Executable)) +} + +// FilePathJoin mocks base method. +func (m *FinchFinderDeps) FilePathJoin(arg0 ...string) string { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FilePathJoin", varargs...) + ret0, _ := ret[0].(string) + return ret0 +} + +// FilePathJoin indicates an expected call of FilePathJoin. +func (mr *FinchFinderDepsMockRecorder) FilePathJoin(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilePathJoin", reflect.TypeOf((*FinchFinderDeps)(nil).FilePathJoin), arg0...) +} diff --git a/pkg/mocks/lima_cmd_creator_system_deps.go b/pkg/mocks/lima_cmd_creator_system_deps.go new file mode 100644 index 000000000..89db0b03a --- /dev/null +++ b/pkg/mocks/lima_cmd_creator_system_deps.go @@ -0,0 +1,108 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/command (interfaces: LimaCmdCreatorSystemDeps) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + os "os" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// LimaCmdCreatorSystemDeps is a mock of LimaCmdCreatorSystemDeps interface. +type LimaCmdCreatorSystemDeps struct { + ctrl *gomock.Controller + recorder *LimaCmdCreatorSystemDepsMockRecorder +} + +// LimaCmdCreatorSystemDepsMockRecorder is the mock recorder for LimaCmdCreatorSystemDeps. +type LimaCmdCreatorSystemDepsMockRecorder struct { + mock *LimaCmdCreatorSystemDeps +} + +// NewLimaCmdCreatorSystemDeps creates a new mock instance. +func NewLimaCmdCreatorSystemDeps(ctrl *gomock.Controller) *LimaCmdCreatorSystemDeps { + mock := &LimaCmdCreatorSystemDeps{ctrl: ctrl} + mock.recorder = &LimaCmdCreatorSystemDepsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *LimaCmdCreatorSystemDeps) EXPECT() *LimaCmdCreatorSystemDepsMockRecorder { + return m.recorder +} + +// Env mocks base method. +func (m *LimaCmdCreatorSystemDeps) Env(arg0 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Env", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// Env indicates an expected call of Env. +func (mr *LimaCmdCreatorSystemDepsMockRecorder) Env(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Env", reflect.TypeOf((*LimaCmdCreatorSystemDeps)(nil).Env), arg0) +} + +// Environ mocks base method. +func (m *LimaCmdCreatorSystemDeps) Environ() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Environ") + ret0, _ := ret[0].([]string) + return ret0 +} + +// Environ indicates an expected call of Environ. +func (mr *LimaCmdCreatorSystemDepsMockRecorder) Environ() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Environ", reflect.TypeOf((*LimaCmdCreatorSystemDeps)(nil).Environ)) +} + +// Stderr mocks base method. +func (m *LimaCmdCreatorSystemDeps) Stderr() *os.File { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stderr") + ret0, _ := ret[0].(*os.File) + return ret0 +} + +// Stderr indicates an expected call of Stderr. +func (mr *LimaCmdCreatorSystemDepsMockRecorder) Stderr() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stderr", reflect.TypeOf((*LimaCmdCreatorSystemDeps)(nil).Stderr)) +} + +// Stdin mocks base method. +func (m *LimaCmdCreatorSystemDeps) Stdin() *os.File { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stdin") + ret0, _ := ret[0].(*os.File) + return ret0 +} + +// Stdin indicates an expected call of Stdin. +func (mr *LimaCmdCreatorSystemDepsMockRecorder) Stdin() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stdin", reflect.TypeOf((*LimaCmdCreatorSystemDeps)(nil).Stdin)) +} + +// Stdout mocks base method. +func (m *LimaCmdCreatorSystemDeps) Stdout() *os.File { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stdout") + ret0, _ := ret[0].(*os.File) + return ret0 +} + +// Stdout indicates an expected call of Stdout. +func (mr *LimaCmdCreatorSystemDepsMockRecorder) Stdout() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stdout", reflect.TypeOf((*LimaCmdCreatorSystemDeps)(nil).Stdout)) +} diff --git a/pkg/mocks/logger.go b/pkg/mocks/logger.go new file mode 100644 index 000000000..459ad9732 --- /dev/null +++ b/pkg/mocks/logger.go @@ -0,0 +1,197 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/flog (interfaces: Logger) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + flog "github.com/runfinch/finch/pkg/flog" +) + +// Logger is a mock of Logger interface. +type Logger struct { + ctrl *gomock.Controller + recorder *LoggerMockRecorder +} + +// LoggerMockRecorder is the mock recorder for Logger. +type LoggerMockRecorder struct { + mock *Logger +} + +// NewLogger creates a new mock instance. +func NewLogger(ctrl *gomock.Controller) *Logger { + mock := &Logger{ctrl: ctrl} + mock.recorder = &LoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Logger) EXPECT() *LoggerMockRecorder { + return m.recorder +} + +// Debugf mocks base method. +func (m *Logger) Debugf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *LoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*Logger)(nil).Debugf), varargs...) +} + +// Debugln mocks base method. +func (m *Logger) Debugln(arg0 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugln", varargs...) +} + +// Debugln indicates an expected call of Debugln. +func (mr *LoggerMockRecorder) Debugln(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugln", reflect.TypeOf((*Logger)(nil).Debugln), arg0...) +} + +// Error mocks base method. +func (m *Logger) Error(arg0 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Error indicates an expected call of Error. +func (mr *LoggerMockRecorder) Error(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*Logger)(nil).Error), arg0...) +} + +// Errorf mocks base method. +func (m *Logger) Errorf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *LoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*Logger)(nil).Errorf), varargs...) +} + +// Fatal mocks base method. +func (m *Logger) Fatal(arg0 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Fatal", varargs...) +} + +// Fatal indicates an expected call of Fatal. +func (mr *LoggerMockRecorder) Fatal(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*Logger)(nil).Fatal), arg0...) +} + +// Info mocks base method. +func (m *Logger) Info(arg0 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Info indicates an expected call of Info. +func (mr *LoggerMockRecorder) Info(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*Logger)(nil).Info), arg0...) +} + +// Infof mocks base method. +func (m *Logger) Infof(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Infof", varargs...) +} + +// Infof indicates an expected call of Infof. +func (mr *LoggerMockRecorder) Infof(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*Logger)(nil).Infof), varargs...) +} + +// Infoln mocks base method. +func (m *Logger) Infoln(arg0 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Infoln", varargs...) +} + +// Infoln indicates an expected call of Infoln. +func (mr *LoggerMockRecorder) Infoln(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infoln", reflect.TypeOf((*Logger)(nil).Infoln), arg0...) +} + +// SetLevel mocks base method. +func (m *Logger) SetLevel(arg0 flog.Level) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLevel", arg0) +} + +// SetLevel indicates an expected call of SetLevel. +func (mr *LoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*Logger)(nil).SetLevel), arg0) +} + +// Warnln mocks base method. +func (m *Logger) Warnln(arg0 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warnln", varargs...) +} + +// Warnln indicates an expected call of Warnln. +func (mr *LoggerMockRecorder) Warnln(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnln", reflect.TypeOf((*Logger)(nil).Warnln), arg0...) +} diff --git a/pkg/mocks/pkg_config_lima_config_applier.go b/pkg/mocks/pkg_config_lima_config_applier.go new file mode 100644 index 000000000..045bf0f98 --- /dev/null +++ b/pkg/mocks/pkg_config_lima_config_applier.go @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/config (interfaces: LimaConfigApplier) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// LimaConfigApplier is a mock of LimaConfigApplier interface. +type LimaConfigApplier struct { + ctrl *gomock.Controller + recorder *LimaConfigApplierMockRecorder +} + +// LimaConfigApplierMockRecorder is the mock recorder for LimaConfigApplier. +type LimaConfigApplierMockRecorder struct { + mock *LimaConfigApplier +} + +// NewLimaConfigApplier creates a new mock instance. +func NewLimaConfigApplier(ctrl *gomock.Controller) *LimaConfigApplier { + mock := &LimaConfigApplier{ctrl: ctrl} + mock.recorder = &LimaConfigApplierMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *LimaConfigApplier) EXPECT() *LimaConfigApplierMockRecorder { + return m.recorder +} + +// Apply mocks base method. +func (m *LimaConfigApplier) Apply() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Apply") + ret0, _ := ret[0].(error) + return ret0 +} + +// Apply indicates an expected call of Apply. +func (mr *LimaConfigApplierMockRecorder) Apply() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*LimaConfigApplier)(nil).Apply)) +} diff --git a/pkg/mocks/pkg_config_load_system_deps.go b/pkg/mocks/pkg_config_load_system_deps.go new file mode 100644 index 000000000..7ef59a848 --- /dev/null +++ b/pkg/mocks/pkg_config_load_system_deps.go @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/config (interfaces: LoadSystemDeps) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// LoadSystemDeps is a mock of LoadSystemDeps interface. +type LoadSystemDeps struct { + ctrl *gomock.Controller + recorder *LoadSystemDepsMockRecorder +} + +// LoadSystemDepsMockRecorder is the mock recorder for LoadSystemDeps. +type LoadSystemDepsMockRecorder struct { + mock *LoadSystemDeps +} + +// NewLoadSystemDeps creates a new mock instance. +func NewLoadSystemDeps(ctrl *gomock.Controller) *LoadSystemDeps { + mock := &LoadSystemDeps{ctrl: ctrl} + mock.recorder = &LoadSystemDepsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *LoadSystemDeps) EXPECT() *LoadSystemDepsMockRecorder { + return m.recorder +} + +// NumCPU mocks base method. +func (m *LoadSystemDeps) NumCPU() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NumCPU") + ret0, _ := ret[0].(int) + return ret0 +} + +// NumCPU indicates an expected call of NumCPU. +func (mr *LoadSystemDepsMockRecorder) NumCPU() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumCPU", reflect.TypeOf((*LoadSystemDeps)(nil).NumCPU)) +} diff --git a/pkg/mocks/pkg_config_nerdctl_config_applier.go b/pkg/mocks/pkg_config_nerdctl_config_applier.go new file mode 100644 index 000000000..0ace29ad3 --- /dev/null +++ b/pkg/mocks/pkg_config_nerdctl_config_applier.go @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/config (interfaces: NerdctlConfigApplier) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// NerdctlConfigApplier is a mock of NerdctlConfigApplier interface. +type NerdctlConfigApplier struct { + ctrl *gomock.Controller + recorder *NerdctlConfigApplierMockRecorder +} + +// NerdctlConfigApplierMockRecorder is the mock recorder for NerdctlConfigApplier. +type NerdctlConfigApplierMockRecorder struct { + mock *NerdctlConfigApplier +} + +// NewNerdctlConfigApplier creates a new mock instance. +func NewNerdctlConfigApplier(ctrl *gomock.Controller) *NerdctlConfigApplier { + mock := &NerdctlConfigApplier{ctrl: ctrl} + mock.recorder = &NerdctlConfigApplierMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *NerdctlConfigApplier) EXPECT() *NerdctlConfigApplierMockRecorder { + return m.recorder +} + +// Apply mocks base method. +func (m *NerdctlConfigApplier) Apply(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Apply", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Apply indicates an expected call of Apply. +func (mr *NerdctlConfigApplierMockRecorder) Apply(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*NerdctlConfigApplier)(nil).Apply), arg0) +} diff --git a/pkg/mocks/pkg_config_nerdctl_config_applier_system_deps.go b/pkg/mocks/pkg_config_nerdctl_config_applier_system_deps.go new file mode 100644 index 000000000..211080dd3 --- /dev/null +++ b/pkg/mocks/pkg_config_nerdctl_config_applier_system_deps.go @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/config (interfaces: NerdctlConfigApplierSystemDeps) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// NerdctlConfigApplierSystemDeps is a mock of NerdctlConfigApplierSystemDeps interface. +type NerdctlConfigApplierSystemDeps struct { + ctrl *gomock.Controller + recorder *NerdctlConfigApplierSystemDepsMockRecorder +} + +// NerdctlConfigApplierSystemDepsMockRecorder is the mock recorder for NerdctlConfigApplierSystemDeps. +type NerdctlConfigApplierSystemDepsMockRecorder struct { + mock *NerdctlConfigApplierSystemDeps +} + +// NewNerdctlConfigApplierSystemDeps creates a new mock instance. +func NewNerdctlConfigApplierSystemDeps(ctrl *gomock.Controller) *NerdctlConfigApplierSystemDeps { + mock := &NerdctlConfigApplierSystemDeps{ctrl: ctrl} + mock.recorder = &NerdctlConfigApplierSystemDepsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *NerdctlConfigApplierSystemDeps) EXPECT() *NerdctlConfigApplierSystemDepsMockRecorder { + return m.recorder +} + +// Env mocks base method. +func (m *NerdctlConfigApplierSystemDeps) Env(arg0 string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Env", arg0) + ret0, _ := ret[0].(string) + return ret0 +} + +// Env indicates an expected call of Env. +func (mr *NerdctlConfigApplierSystemDepsMockRecorder) Env(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Env", reflect.TypeOf((*NerdctlConfigApplierSystemDeps)(nil).Env), arg0) +} diff --git a/pkg/mocks/pkg_dependency_dependency.go b/pkg/mocks/pkg_dependency_dependency.go new file mode 100644 index 000000000..518767cc8 --- /dev/null +++ b/pkg/mocks/pkg_dependency_dependency.go @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/dependency (interfaces: Dependency) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// Dependency is a mock of Dependency interface. +type Dependency struct { + ctrl *gomock.Controller + recorder *DependencyMockRecorder +} + +// DependencyMockRecorder is the mock recorder for Dependency. +type DependencyMockRecorder struct { + mock *Dependency +} + +// NewDependency creates a new mock instance. +func NewDependency(ctrl *gomock.Controller) *Dependency { + mock := &Dependency{ctrl: ctrl} + mock.recorder = &DependencyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Dependency) EXPECT() *DependencyMockRecorder { + return m.recorder +} + +// Install mocks base method. +func (m *Dependency) Install() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Install") + ret0, _ := ret[0].(error) + return ret0 +} + +// Install indicates an expected call of Install. +func (mr *DependencyMockRecorder) Install() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Install", reflect.TypeOf((*Dependency)(nil).Install)) +} + +// Installed mocks base method. +func (m *Dependency) Installed() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Installed") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Installed indicates an expected call of Installed. +func (mr *DependencyMockRecorder) Installed() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Installed", reflect.TypeOf((*Dependency)(nil).Installed)) +} + +// RequiresRoot mocks base method. +func (m *Dependency) RequiresRoot() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RequiresRoot") + ret0, _ := ret[0].(bool) + return ret0 +} + +// RequiresRoot indicates an expected call of RequiresRoot. +func (mr *DependencyMockRecorder) RequiresRoot() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequiresRoot", reflect.TypeOf((*Dependency)(nil).RequiresRoot)) +} diff --git a/pkg/mocks/pkg_fmemory_memory.go b/pkg/mocks/pkg_fmemory_memory.go new file mode 100644 index 000000000..5b5f59bdf --- /dev/null +++ b/pkg/mocks/pkg_fmemory_memory.go @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/fmemory (interfaces: Memory) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// Memory is a mock of Memory interface. +type Memory struct { + ctrl *gomock.Controller + recorder *MemoryMockRecorder +} + +// MemoryMockRecorder is the mock recorder for Memory. +type MemoryMockRecorder struct { + mock *Memory +} + +// NewMemory creates a new mock instance. +func NewMemory(ctrl *gomock.Controller) *Memory { + mock := &Memory{ctrl: ctrl} + mock.recorder = &MemoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Memory) EXPECT() *MemoryMockRecorder { + return m.recorder +} + +// TotalMemory mocks base method. +func (m *Memory) TotalMemory() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TotalMemory") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// TotalMemory indicates an expected call of TotalMemory. +func (mr *MemoryMockRecorder) TotalMemory() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalMemory", reflect.TypeOf((*Memory)(nil).TotalMemory)) +} diff --git a/pkg/mocks/pkg_ssh_dialer.go b/pkg/mocks/pkg_ssh_dialer.go new file mode 100644 index 000000000..6668ca85a --- /dev/null +++ b/pkg/mocks/pkg_ssh_dialer.go @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch/pkg/fssh (interfaces: Dialer) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + ssh "golang.org/x/crypto/ssh" +) + +// Dialer is a mock of Dialer interface. +type Dialer struct { + ctrl *gomock.Controller + recorder *DialerMockRecorder +} + +// DialerMockRecorder is the mock recorder for Dialer. +type DialerMockRecorder struct { + mock *Dialer +} + +// NewDialer creates a new mock instance. +func NewDialer(ctrl *gomock.Controller) *Dialer { + mock := &Dialer{ctrl: ctrl} + mock.recorder = &DialerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Dialer) EXPECT() *DialerMockRecorder { + return m.recorder +} + +// Dial mocks base method. +func (m *Dialer) Dial(arg0, arg1 string, arg2 *ssh.ClientConfig) (*ssh.Client, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Dial", arg0, arg1, arg2) + ret0, _ := ret[0].(*ssh.Client) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Dial indicates an expected call of Dial. +func (mr *DialerMockRecorder) Dial(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dial", reflect.TypeOf((*Dialer)(nil).Dial), arg0, arg1, arg2) +} diff --git a/pkg/path/finch.go b/pkg/path/finch.go new file mode 100644 index 000000000..e46d5a2fe --- /dev/null +++ b/pkg/path/finch.go @@ -0,0 +1,81 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package path contains functions to find/calculate path used in the project. +package path + +import ( + "fmt" + + "github.com/runfinch/finch/pkg/system" +) + +// Finch provides a set of methods that calculate paths relative to the Finch path. +type Finch string + +// ConfigFilePath returns the path to Finch config file. +func (Finch) ConfigFilePath(homeDir string) string { + return fmt.Sprintf("%s/.finch/finch.yaml", homeDir) +} + +// LimaHomePath returns the path that should be set to LIMA_HOME for Finch. +func (w Finch) LimaHomePath() string { + return fmt.Sprintf("%s/lima/data", w) +} + +// LimactlPath returns the limactl path. +func (w Finch) LimactlPath() string { + return fmt.Sprintf("%s/lima/bin/limactl", w) +} + +// QEMUBinDir returns the path to the directory that contains all the binaries QEMU depends on. +// It's used to enable users to always use the pinned versions of the binaries. +func (w Finch) QEMUBinDir() string { + return fmt.Sprintf("%s/lima/bin", w) +} + +// BaseYamlFilePath returns the base yaml file path. +func (w Finch) BaseYamlFilePath() string { + return fmt.Sprintf("%s/os/finch.yaml", w) +} + +// LimaConfigDirectoryPath returns the lima config directory path. +func (w Finch) LimaConfigDirectoryPath() string { + return fmt.Sprintf("%s/lima/data/_config", w) +} + +// LimaOverrideConfigPath returns the lima override config file path. +func (w Finch) LimaOverrideConfigPath() string { + return fmt.Sprintf("%s/lima/data/_config/override.yaml", w) +} + +// LimaSSHPrivateKeyPath returns the lima user key path. +func (w Finch) LimaSSHPrivateKeyPath() string { + return fmt.Sprintf("%s/lima/data/_config/user", w) +} + +// FinchFinderDeps provides all the dependencies FindFinch needs to find Finch. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../mocks/finch_finder_deps.go -package=mocks -mock_names FinchFinderDeps=FinchFinderDeps . FinchFinderDeps +type FinchFinderDeps interface { + system.SymlinksEvaluator + system.ExecutableFinder + system.FilePathJoiner + system.EnvGetter +} + +// FindFinch finds the installation path of Finch. +func FindFinch(deps FinchFinderDeps) (Finch, error) { + exe, err := deps.Executable() + if err != nil { + return "", fmt.Errorf("failed to locate the executable that starts this process: %w", err) + } + realPath, err := deps.EvalSymlinks(exe) + if err != nil { + return "", fmt.Errorf("failed to find the real path of the executable: %w", err) + } + // The directory structure is finch_home/bin/finch, + // where the last path comment (i.e., finch) is the executable that starts this process. + res := deps.FilePathJoin(realPath, "../../") + return Finch(res), nil +} diff --git a/pkg/path/finch_test.go b/pkg/path/finch_test.go new file mode 100644 index 000000000..f7e514d77 --- /dev/null +++ b/pkg/path/finch_test.go @@ -0,0 +1,128 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package path + +import ( + "errors" + "fmt" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +var mockFinch = Finch("mock_finch") + +func TestFinch_ConfigFilePath(t *testing.T) { + t.Parallel() + + res := mockFinch.ConfigFilePath("homeDir") + assert.Equal(t, res, "homeDir/.finch/finch.yaml") +} + +func TestFinch_LimaHomePath(t *testing.T) { + t.Parallel() + + res := mockFinch.LimaHomePath() + assert.Equal(t, res, "mock_finch/lima/data") +} + +func TestFinch_LimactlPath(t *testing.T) { + t.Parallel() + + res := mockFinch.LimactlPath() + assert.Equal(t, res, "mock_finch/lima/bin/limactl") +} + +func TestFinch_BaseYamlFilePath(t *testing.T) { + t.Parallel() + + res := mockFinch.BaseYamlFilePath() + assert.Equal(t, res, "mock_finch/os/finch.yaml") +} + +func TestFinch_LimaConfigDirectoryPath(t *testing.T) { + t.Parallel() + + res := mockFinch.LimaConfigDirectoryPath() + assert.Equal(t, res, "mock_finch/lima/data/_config") +} + +func TestFinch_LimaOverrideConfigPath(t *testing.T) { + t.Parallel() + + res := mockFinch.LimaOverrideConfigPath() + assert.Equal(t, res, "mock_finch/lima/data/_config/override.yaml") +} + +func TestFinch_LimaSSHPrivateKeyPath(t *testing.T) { + t.Parallel() + + res := mockFinch.LimaSSHPrivateKeyPath() + assert.Equal(t, res, "mock_finch/lima/data/_config/user") +} + +func TestFinch_QemuBinDir(t *testing.T) { + t.Parallel() + + res := mockFinch.QEMUBinDir() + assert.Equal(t, res, "mock_finch/lima/bin") +} + +func TestFindFinch(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.FinchFinderDeps) + wantErr error + want Finch + }{ + { + name: "happy path", + wantErr: nil, + want: Finch("/real"), + mockSvc: func(deps *mocks.FinchFinderDeps) { + deps.EXPECT().Executable().Return("/bin/path", nil) + deps.EXPECT().EvalSymlinks("/bin/path").Return("/real/bin/path", nil) + deps.EXPECT().FilePathJoin("/real/bin/path", "../../").Return("/real") + }, + }, + { + name: "failed to find the executable path", + want: "", + wantErr: fmt.Errorf("failed to locate the executable that starts this process: %w", + errors.New("failed to find executable path"), + ), + mockSvc: func(deps *mocks.FinchFinderDeps) { + deps.EXPECT().Executable().Return("", errors.New("failed to find executable path")) + }, + }, + { + name: "failed to find the real path of the executable", + want: "", + wantErr: fmt.Errorf("failed to find the real path of the executable: %w", errors.New("failed to find real path")), + mockSvc: func(deps *mocks.FinchFinderDeps) { + deps.EXPECT().Executable().Return("/bin/path", nil) + deps.EXPECT().EvalSymlinks("/bin/path").Return("", errors.New("failed to find real path")) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + deps := mocks.NewFinchFinderDeps(ctrl) + tc.mockSvc(deps) + got, err := FindFinch(deps) + assert.Equal(t, err, tc.wantErr) + assert.Equal(t, got, tc.want) + }) + } +} diff --git a/pkg/system/stdlib.go b/pkg/system/stdlib.go new file mode 100644 index 000000000..8dde8d2f4 --- /dev/null +++ b/pkg/system/stdlib.go @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package system + +import ( + "os" + "path/filepath" + "runtime" +) + +// StdLib implements the interfaces defined in system.go via standard library functions. +type StdLib struct{} + +//revive:disable:exported The exported functions below are straightforward. + +func NewStdLib() *StdLib { + return &StdLib{} +} + +func (s *StdLib) EvalSymlinks(path string) (string, error) { + return filepath.EvalSymlinks(path) +} + +func (s *StdLib) FilePathJoin(elem ...string) string { + return filepath.Join(elem...) +} + +func (s *StdLib) Executable() (string, error) { + return os.Executable() +} + +func (s *StdLib) Environ() []string { + return os.Environ() +} + +func (s *StdLib) Env(key string) string { + return os.Getenv(key) +} + +func (s *StdLib) Stdin() *os.File { + return os.Stdin +} + +func (s *StdLib) Stdout() *os.File { + return os.Stdout +} + +func (s *StdLib) Stderr() *os.File { + return os.Stderr +} + +func (s *StdLib) NumCPU() int { + return runtime.NumCPU() +} + +func (s *StdLib) ReadMemStats(st *runtime.MemStats) { + runtime.ReadMemStats(st) +} diff --git a/pkg/system/system.go b/pkg/system/system.go new file mode 100644 index 000000000..985a9e060 --- /dev/null +++ b/pkg/system/system.go @@ -0,0 +1,68 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package system contains interfaces for OS related APIs, +// intended to be used for dependency injection to facilitate unit testing. +// +// The naming of the methods in this package is not very Go-idiomatic because +// the user of this package usually composes the interfaces in this package into one interface. +// +// For example, the `Executable` method of `ExecutableFinder` should have been named as `Find` according to Go idiom, +// but it will be unreadable if the interface is embedded in another interface. For instance, +// +// type SystemDeps interface { +// system.ExecutableFinder +// // other methods +// } +// var deps SystemDeps +// deps.Executable() // in favor of deps.Find() +package system + +import ( + "os" +) + +// SymlinksEvaluator mocks out filepath.EvalSymlinks. +type SymlinksEvaluator interface { + EvalSymlinks(path string) (string, error) +} + +// ExecutableFinder mocks out os.Executable. +type ExecutableFinder interface { + Executable() (string, error) +} + +// FilePathJoiner mocks out filepath.Join. +type FilePathJoiner interface { + FilePathJoin(elem ...string) string +} + +// EnvironGetter mocks out os.Environ. +type EnvironGetter interface { + Environ() []string +} + +// EnvGetter mocks out os.Getenv. +type EnvGetter interface { + Env(key string) string +} + +// StdinGetter mocks out os.Stdin. +type StdinGetter interface { + Stdin() *os.File +} + +// StdoutGetter mocks out os.Stdout. +type StdoutGetter interface { + Stdout() *os.File +} + +// StderrGetter mocks out os.Stderr. +type StderrGetter interface { + Stderr() *os.File +} + +// RuntimeCPUGetter mocks out runtime.NumCPU. +type RuntimeCPUGetter interface { + NumCPU() int +} diff --git a/pkg/tools.go b/pkg/tools.go new file mode 100644 index 000000000..18cc4a66b --- /dev/null +++ b/pkg/tools.go @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build tools + +package pkg + +// Ensure that everyone working on this project uses the same version of the following Go-based tools. +// +// For the tutorial to add a new tool, see https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module. +// After following the tutorial, update `download-licenses` in Makefile. You'll likely also need to update `gen-code`. +import ( + _ "github.com/golang/mock/mockgen" + _ "github.com/google/go-licenses" + _ "golang.org/x/tools/cmd/stringer" +) diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 000000000..568eeca32 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,8 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package version contains generated version number from GO build +package version + +// Version will be filled via Makefile. +var Version string