diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5ad499 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +out/ +vendor/ +*.out +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..8be354d --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,38 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + - go mod tidy -compat=1.17 + - wire gen ./... +builds: + - + flags: + - -tags=prod + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - 386 + - amd64 + - arm + - arm64 + binary: recode +archives: + - + # Additional files/template/globs you want to add to the archive. + # Defaults are any files matching `LICENSE*`, `README*`, `CHANGELOG*`, + # `license*`, `readme*` and `changelog*`. + files: +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.recode/hooks/init.sh b/.recode/hooks/init.sh new file mode 100644 index 0000000..ed75049 --- /dev/null +++ b/.recode/hooks/init.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +log () { + echo -e "${1}" >&2 +} + +log "Downloading dependencies listed in go.mod" + +go mod download diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f04c280 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go", + "args": [ + "aws", + "--profile", + "production", + "--region", + "eu-west-3", + "start", + "recode-sh/workspace", + //"--rebuild", + ] + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..305962a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..389d159 --- /dev/null +++ b/README.md @@ -0,0 +1,555 @@ +

+ recode +

+ +

+

Recode

+

Remote development environments defined as code. Running on your cloud provider account.
Currently available on Amazon Web Services and Visual Studio Code.

+

+ +```bash +recode aws start recode-sh/workspace --instance-type t2.medium +``` + +
+ ... see the recode-sh/workspace repository for an example of development environment configuration +
+ +![vscode](assets/vscode.png) + +## Table of contents +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) + - [Login](#login) + - [Start](#start) + - [Stop](#stop) + - [Remove](#remove) + - [Uninstall](#uninstall) +- [Development environments configuration](#development-environments-configuration) + - [Tip: the --rebuild flag](#-tip-the---rebuild-flag) + - [Tip: Docker & Docker compose](#-tip-docker--docker-compose) + - [User configuration](#user-configuration) + - [Project configuration](#project-configuration) + - [What if I don't have an user configuration?](#-what-if-i-dont-have-created-an-user-configuration) + - [Recode configuration](#recode-configuration) + - [Base image (recode-sh/base-dev-env)](#base-image-recode-shbase-dev-env) + - [Visual Studio Code extensions](#visual-studio-code-extensions) + - [Multiple repositories](#multiple-repositories) + - [Build arguments (RECODE_INSTANCE_OS and RECODE_INSTANCE_ARCH)](#build-arguments-recode_instance_os-and-recode_instance_arch) + - [Hooks](#hooks) +- [Frequently asked questions](#frequently-asked-questions) + - [How does it compare with GitPod/Coder/Codespaces/X?](#how-does-it-compare-with-gitpodcodercodespacesx) + - [How does it compare with VSCode remote SSH / Container extensions?](#how-does-it-compare-with-vscode-remote-ssh--container-extensions) +- [The future](#the-future) +- [License](#license) + +## Requirements + +The Recode binary has been tested on Linux and Mac. Support for Windows is theoretical ([testers needed](https://code.visualstudio.com/) 💙). + +Before using Recode, the following dependencies need to be installed: + +- [Visual Studio Code](https://code.visualstudio.com/) (currently the sole editor supported). + +- [OpenSSH Client](https://www.openssh.com/) (used to access your development environments). + +## Installation + +The easiest way to get started is by running the following command in your terminal: + +```bash +curl -sf https://raw.githubusercontent.com/recode-sh/cli/main/install.sh | sh +``` + +You could confirm that Recode is installed by running the `recode` command: + +```bash +recode --help +``` + +## Usage + +```console +To begin, run the command "recode login" to connect your GitHub account. + +From there, the most common workflow is: + + - recode start : to start a development environment for a specific GitHub repository + - recode stop : to stop a development environment (without removing your data) + - recode remove : to remove a development environment AND your data + + may be relative to your personal GitHub account (eg: cli) or fully qualified (eg: my-organization/api). + +Usage: + recode [command] + +Available Commands: + aws Use Recode on Amazon Web Services + completion Generate the autocompletion script for the specified shell + help Help about any command + login Connect a GitHub account to use with Recode + +Flags: + -h, --help help for recode + +Use "recode [command] --help" for more information about a command. +``` + +### Login + +```bash +recode login +``` +To begin, you need to run the `login` command to connect your GitHub account. + +Recode requires the following permissions: + + - "*Public SSH keys*" and "*Repositories*" to let you access your repositories from your development environments. + + - "*GPG Keys*" and "*Personal user data*" to configure Git and sign your commits (verified badge). + +**All your data (including the OAuth access token) are only stored locally in `~/.config/recode/recode.yml` (or in `XDG_CONFIG_HOME` if set).** + +The source code that implement the GitHub OAuth flow is located in the [recode-sh/api](https://github.com/recode-sh/api) repository. + +### Start + +```bash +recode start +``` +The `start` command creates and starts a development environment for a specific GitHub repository. + +If a development environment is stopped, it will only be started. If a development environment is already started, only your code editor will be opened. + +An `--instance-type` flag could be passed to specify the instance type that will power your development environment. (*See the corresponding cloud provider repository for default / valid values*). + +#### Examples + +```bash +recode aws start recode-sh/workspace +``` + +```bash +recode aws start recode-sh/workspace --instance-type t2.medium +``` + +### Stop + +```bash +recode stop +``` +The `stop` command stops a started development environment. + +Stopping means that the underlying instance will be stopped but **your data will be conserved**. You may want to use this command to save costs when the development environment is not used. + +#### Example + +```bash +recode aws stop recode-sh/workspace +``` + +### Remove + +```bash +recode remove +``` + +The `remove` command removes an existing development environment. + +Removing means that the underlying instance **and all your data** will be **permanently removed**. + +#### Example + +```bash +recode aws remove recode-sh/workspace +``` + +### Uninstall + +```bash +recode uninstall +``` + +The `uninstall` command removes all the infrastructure components used by Recode from your cloud provider account. (*See the corresponding cloud provider repository for details*). + +**Before running this command, all development environments need to be removed.** + +#### Example + +```bash +recode aws uninstall +``` + +## Development environments configuration + +If you think about all the projects you've worked on, you may notice that you've: + + - a set of configuration / tools used for all your projects (eg: a prefered timezone / locale, a specific shell...); + + - a set of configuration / tools specific for each project (eg: docker compose, go >= 1.18 or node.js >= 14). + +This is what Recode has tried to mimic with *user* and *project* configuration. + +#### 💡 Tip: the `--rebuild` flag + +```bash +recode aws start recode-sh/workspace --rebuild +``` + +If you update the configuration of an existing development environment, you could use the `--rebuild` flag of the `start` command to rebuild it without having to delete it first. + +#### 💡 Tip: Docker & Docker compose + +Docker and Docker compose are already preinstalled in all development environments so you don't have to install them. + +### User configuration + +User configuration corresponds to the set of configuration / tools used for all your projects. To create an user configuration, all you need to do is to: + + 1. Create a **repository** named `.recode` in your personal GitHub account. + + 2. Add a file named `dev_env.Dockerfile` in it. + +The file `dev_env.Dockerfile` is a regular Dockerfile except that: + + - it must derive from `recode-sh/base-dev-env` (more below); + + - the user configuration needs to be applied to the user `recode`. + +Otherwise, you are free to do what you want with this file and this repository. You could see an example with dotfiles in [recode-sh/.recode](https://github.com/recode-sh/.recode) and use it as a GitHub repository template: + +```Dockerfile +# User's dev env image must derive from recodesh/base-dev-env. +# See https://github.com/recode-sh/base-dev-env/blob/main/Dockerfile for source. +FROM recodesh/base-dev-env:latest + +# Set timezone +ENV TZ=America/Los_Angeles + +# Set locale +RUN sudo locale-gen en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +# Install Zsh +RUN set -euo pipefail \ + && sudo apt-get --assume-yes --quiet --quiet update \ + && sudo apt-get --assume-yes --quiet --quiet install zsh \ + && sudo rm --recursive --force /var/lib/apt/lists/* + +# Install OhMyZSH and some plugins +RUN set -euo pipefail \ + && sh -c "$(curl --fail --silent --show-error --location https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ + && git clone --quiet https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions \ + && git clone --quiet https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting + +# Change default shell for user "recode" +RUN set -euo pipefail \ + && sudo usermod --shell $(which zsh) recode + +# Add all dotfiles to home folder +COPY --chown=recode:recode ./dotfiles/.* $HOME/ +``` + +### Project configuration + +Project configuration corresponds to the set of configuration / tools specific for each project. As you may have guessed, to create a project configuration, all you need to do is to: + + 1. Create a **directory** named `.recode` in your project's repository. + + 2. Add a file named `dev_env.Dockerfile` in it. + +The file `dev_env.Dockerfile` is a regular Dockerfile except that: + + - it must derive from `user_dev_env` (your user configuration); + + - the user configuration needs to be applied to the user `recode`. + +Otherwise, you are free to do what you want with this file and this directory. You could see an example in [recode-sh/workspace](https://github.com/recode-sh/workspace): + +```Dockerfile +# Project's dev env image must derive from "user_dev_env" +# (ie: github_user_name/.recode/dev_env.Dockerfile) +FROM user_dev_env + +# VSCode extensions that need to be installed (optional) +LABEL sh.recode.vscode.extensions="golang.go, zxh404.vscode-proto3, ms-azuretools.vscode-docker" + +# GitHub repositories that need to be cloned (optional) (default to the current one) +LABEL sh.recode.repositories="cli, agent, recode, aws-cloud-provider, base-dev-env, api, .recode, workspace" + +# Reserved args (RECODE_*). Provided by Recode. +# eg: linux +ARG RECODE_INSTANCE_OS +# eg: amd64 or arm64 +ARG RECODE_INSTANCE_ARCH + +ARG GO_VERSION=1.18.2 + +# Install Go and dev dependencies +RUN set -euo pipefail \ + && cd /tmp \ + && LATEST_GO_VERSION=$(curl --fail --silent --show-error --location "https://golang.org/VERSION?m=text") \ + && if [[ "${GO_VERSION}" = "latest" ]] ; then \ + GO_VERSION_TO_USE="${LATEST_GO_VERSION}" ; \ + else \ + GO_VERSION_TO_USE="go${GO_VERSION}" ; \ + fi \ + && curl --fail --silent --show-error --location "https://go.dev/dl/${GO_VERSION_TO_USE}.${RECODE_INSTANCE_OS}-${RECODE_INSTANCE_ARCH}.tar.gz" --output go.tar.gz \ + && sudo tar --directory /usr/local --extract --file go.tar.gz \ + && rm go.tar.gz \ + && /usr/local/go/bin/go install golang.org/x/tools/cmd/goimports@latest \ + && /usr/local/go/bin/go install github.com/google/wire/cmd/wire@latest \ + && /usr/local/go/bin/go install github.com/golang/mock/mockgen@latest + +# Add Go to path +ENV PATH=$PATH:/usr/local/go/bin:$HOME/go/bin + +... +``` +#### 💡 What if I don't have an user configuration? + +If you don't have an user configuration, **the [recode-sh/.recode](https://github.com/recode-sh/.recode) repository will be used as a default one**. + +That's why you will have `zsh` configured as default shell in your project. + +### Recode configuration + +As you may have noticed from previous sections, some commands in the `dev_env.Dockerfile` files (like the `LABEL` ones) are specific to Recode. This section will try to explain them. + +#### Base image ([recode-sh/base-dev-env](http://github.com/recode-sh/base-dev-env)) + +As you may have understood, all the development environments derive directly or indirectly from `recode-sh/base-dev-env`. You could see the source of this Docker image in the [recode-sh/base-dev-env](https://github.com/recode-sh/base-dev-env) repository: + +```Dockerfile +# All development environments will be Ubuntu-based +FROM ubuntu:22.04 + +ARG DEBIAN_FRONTEND=noninteractive + +# RUN will use bash +SHELL ["/bin/bash", "-c"] + +# Install system dependencies +RUN set -euo pipefail \ + && apt-get --assume-yes --quiet --quiet update \ + && apt-get --assume-yes --quiet --quiet install \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + git \ + gnupg \ + locales \ + lsb-release \ + nano \ + sudo \ + unzip \ + vim \ + wget \ + && rm --recursive --force /var/lib/apt/lists/* + +# Install the Docker CLI. +# The Docker daemon socket will be mounted from instance. +RUN set -euo pipefail \ + && curl --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt-get --assume-yes --quiet --quiet update \ + && apt-get --assume-yes --quiet --quiet install docker-ce-cli \ + && rm --recursive --force /var/lib/apt/lists/* + +# Install Docker compose +RUN set -euo pipefail \ + && LATEST_COMPOSE_VERSION=$(curl --fail --silent --show-error --location "https://api.github.com/repos/docker/compose/releases/latest" | grep --only-matching --perl-regexp '(?<="tag_name": ").+(?=")') \ + && curl --fail --silent --show-error --location "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-$(uname --kernel-name)-$(uname --machine)" --output /usr/libexec/docker/cli-plugins/docker-compose \ + && chmod +x /usr/libexec/docker/cli-plugins/docker-compose + +# Install entrypoint script +COPY ./recode_entrypoint.sh / +RUN chmod +x /recode_entrypoint.sh + +# Configure the user "recode" in container. +# Triggered during build on instance. +# +# We want the user "recode" inside the container to get +# the same permissions than the user "recode" in the instance +# (to access the Docker daemon, SSH keys and so on). +# +# To do this, the two users need to share the same UID/GID. +ONBUILD ARG RECODE_USER_ID +ONBUILD ARG RECODE_USER_GROUP_ID +ONBUILD ARG RECODE_DOCKER_GROUP_ID + +ONBUILD RUN set -euo pipefail \ + && RECODE_USER_HOME_DIR="/home/recode" \ + && RECODE_USER_WORKSPACE_DIR="${RECODE_USER_HOME_DIR}/workspace" \ + && RECODE_USER_WORKSPACE_CONFIG_DIR="${RECODE_USER_HOME_DIR}/.workspace-config" \ + && groupadd --gid "${RECODE_USER_GROUP_ID}" --non-unique recode \ + && useradd --gid "${RECODE_USER_GROUP_ID}" --uid "${RECODE_USER_ID}" --non-unique --home "${RECODE_USER_HOME_DIR}" --create-home --shell /bin/bash recode \ + && cp /etc/sudoers /etc/sudoers.orig \ + && echo "recode ALL=(ALL) NOPASSWD:ALL" | tee /etc/sudoers.d/recode > /dev/null \ + && groupadd --gid "${RECODE_DOCKER_GROUP_ID}" --non-unique docker \ + && usermod --append --groups docker recode \ + && mkdir --parents "${RECODE_USER_WORKSPACE_CONFIG_DIR}" \ + && mkdir --parents "${RECODE_USER_WORKSPACE_DIR}" \ + && mkdir --parents "${RECODE_USER_HOME_DIR}/.ssh" \ + && mkdir --parents "${RECODE_USER_HOME_DIR}/.gnupg" \ + && mkdir --parents "${RECODE_USER_HOME_DIR}/.vscode-server" \ + && chown --recursive recode:recode "${RECODE_USER_HOME_DIR}" \ + && chmod 700 "${RECODE_USER_HOME_DIR}/.gnupg" + +ONBUILD WORKDIR /home/recode/workspace +ONBUILD USER recode + +ONBUILD ENV USER=recode +ONBUILD ENV HOME=/home/recode +ONBUILD ENV EDITOR=/usr/bin/nano + +ONBUILD ENV RECODE_WORKSPACE=/home/recode/workspace +ONBUILD ENV RECODE_WORKSPACE_CONFIG=/home/recode/.workspace-config + +# Only for documentation purpose. +# Entrypoint and CMD are always set by the +# Recode agent when running the dev env container. +ONBUILD ENTRYPOINT ["/recode_entrypoint.sh"] +ONBUILD CMD ["sleep", "infinity"] + +# Set default timezone +ENV TZ=America/Los_Angeles + +# Set default locale +# /!\ locale-gen must be run as root +RUN locale-gen en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 +``` + +As you can see, nothing fancy here. + +Recode is built on `ubuntu` with `docker` and `docker compose` pre-installed. An user `recode` is created and configured to be used as the default user. Root privileges are managed via `sudo`. + +Your repositories will be cloned in `/home/recode/workspace`. A default timezone and locale are set. + +*(To learn more, see the [recode-sh/base-dev-env](https://github.com/recode-sh/base-dev-env) repository)*. + +#### Visual Studio Code extensions + +In order to require Visual Studio Code extensions to be installed in your development environment, you need to add a `LABEL` named `sh.recode.vscode.extensions` in your *user's* or *project's* `dev_env.Dockerfile`. + +*(As you may have guessed, if this label is added to your user configuration, all your projects will have the listed extensions installed).* + +An extension is identified using its publisher name and extension identifier (`publisher.extension`). You can see the name on the extension's detail page. + +##### Example + +```Dockerfile +LABEL sh.recode.vscode.extensions="golang.go, zxh404.vscode-proto3, ms-azuretools.vscode-docker" +``` + +#### Multiple repositories + +If you want to use multiple repositories in your development environment, you need to add a `LABEL` named `sh.recode.repositories` in your *project's* `dev_env.Dockerfile`. + +**In this case, we recommend you to create an empty repository that will only contain the `.recode` directory (as an example, see the [recode-sh/workspace](https://github.com/recode-sh/workspace) repository).** + +*(As you may have guessed, if this label is added to your user configuration it will be ignored).* + +Repositories may be set as relative to the current one (eg: `cli`) or fully qualified (eg: `recode-sh/cli`). + +##### Example + +```Dockerfile +LABEL sh.recode.repositories="cli, agent, recode, aws-cloud-provider, base-dev-env, api, .recode, workspace" +``` + +#### Build arguments (`RECODE_INSTANCE_OS` and `RECODE_INSTANCE_ARCH`) + +Given the nature of this project, you need to take into account the fact that the characteristics of the instance used to run your development environment may vary depending on the one chosen by the final user. + +As an example, an user may want to use an AWS graviton powered instance to run your project and, as a result, your *project's* `dev_env.Dockerfile` must be ready to be built for `ARM`. + +To ease this process, Recode will pass to your `dev_env.Dockerfile` files two build arguments `RECODE_INSTANCE_OS` and `RECODE_INSTANCE_ARCH` that will contain both the current operating system (`linux`) and architecture (eg: `amd64`) respectively. + +##### Example + +```Dockerfile +# Reserved args (RECODE_*). Provided by Recode. + +# eg: linux +ARG RECODE_INSTANCE_OS + +# eg: amd64 or arm64 +ARG RECODE_INSTANCE_ARCH +``` + +#### Hooks + +Hooks are shell scripts that will be run during the lifetime of your development environment. To be able to add a hook in a project, all you have to do is to add a directory named `hooks` in your `.recode` **directory**. + +##### First Hook + +Before adding your first hook, the following things must be taken into account: + + - In the case of development environments **with only one repository**, hooks will only be run if a `dev_env.Dockerfile` file is set. + + - In the case of development environments **with multiple repositories**, all the hooks will be run, one after the other. + + - **The working directory of your scripts will be set to the root folder of their respective repository before running**. + +##### Init + +The `init` hook is run once, **during the first start of your development environment**. You could use it to download your project dependencies, for example. + +Currently, it's the sole hook available. To activate it, you need to add an `init.sh` file in your project's `hooks` directory. + +##### Example (taken from the [recode-sh/cli](https://github.com/recode-sh/cli/tree/main/.recode) repository) + +```bash +#!/bin/bash +set -euo pipefail + +log () { + echo -e "${1}" >&2 +} + +log "Downloading dependencies listed in go.mod" + +go mod download +``` + +## Frequently asked questions + +#### How does it compare with GitPod/Coder/Codespaces/X? + +- 100% Free. +- 100% Open-source. +- 100% Private (run on your own cloud provider account). +- 100% Cost-effective (run on simple VMs not on Kubernetes). +- 100% Desktop. +- 100% Multi regions. +- 100% Customizable (from VM characteristics to installed runtimes). +- 100% Community-driven (see below). + +... and 0% VC-backed. 0% Locked-in. 0% Proprietary config files. + +#### How does it compare with VSCode remote SSH / Container extensions? + +- Remote development environments defined as code (with support for user and project configuration). +- Automatic infrastructure / VM provisionning for multiple cloud providers. +- Fully integrated with GitHub (private and multiple repositories, verified commits...). +- Support the pre-installation of VSCode extensions. +- Doesn't require Docker to be installed locally. +- Doesn't tied to a specific code editor. + +## The future + +This project is **100% community-driven**, meaning that except for bug fixes **no more features will be added**. + +The only features that will be added are the ones that will be [posted as an issue](https://github.com/recode-sh/cli/issues/new) and that will receive a significant amount of upvotes **(>= 10 currently)**. + +## License + +Recode is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). diff --git a/assets/recode.png b/assets/recode.png new file mode 100644 index 0000000..59d6391 Binary files /dev/null and b/assets/recode.png differ diff --git a/assets/vscode.png b/assets/vscode.png new file mode 100644 index 0000000..e137518 Binary files /dev/null and b/assets/vscode.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ee589fd --- /dev/null +++ b/go.mod @@ -0,0 +1,82 @@ +module github.com/recode-sh/cli + +go 1.17 + +replace github.com/recode-sh/aws-cloud-provider v0.0.0 => ../aws-cloud-provider + +replace github.com/recode-sh/recode v0.0.0 => ../recode + +replace github.com/recode-sh/agent v0.0.0 => ../agent + +require ( + github.com/aws/aws-sdk-go-v2/config v1.13.1 + github.com/briandowns/spinner v1.18.1 + github.com/golang/mock v1.6.0 + github.com/google/go-github/v43 v43.0.0 + github.com/google/wire v0.5.0 + github.com/jwalton/gchalk v1.3.0 + github.com/kevinburke/ssh_config v1.1.0 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 + github.com/recode-sh/agent v0.0.0 + github.com/recode-sh/aws-cloud-provider v0.0.0 + github.com/recode-sh/recode v0.0.0 + github.com/spf13/cobra v1.3.0 + github.com/spf13/viper v1.10.1 + golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000 + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 + google.golang.org/grpc v1.46.2 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.15.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.6.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.11.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.29.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.5.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect + github.com/aws/smithy-go v1.11.1 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gosimple/slug v1.12.0 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jsonmaur/aws-regions/v2 v2.3.1 // indirect + github.com/jwalton/go-supportscolor v1.1.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pelletier/go-toml v1.9.4 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/whilp/git-urls v1.0.0 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7bea1de --- /dev/null +++ b/go.sum @@ -0,0 +1,885 @@ +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.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.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +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/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +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= +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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w= +github.com/aws/aws-sdk-go-v2 v1.15.0 h1:f9kWLNfyCzCB43eupDAk3/XgJ2EpgktiySD6leqs0js= +github.com/aws/aws-sdk-go-v2 v1.15.0/go.mod h1:lJYcuZZEHWNIb6ugJjbQY1fykdoobWbOS7kJYb4APoI= +github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= +github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= +github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o= +github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.6.0 h1:qS/1WpMN7RyJD+qQsS+pwtGxxaRJa3qbf6EP7jZwLIg= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.6.0/go.mod h1:LchVYRkk9AQyRgDXWAlJ01H5C1XcODuPK9/RyeCcIYk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 h1:xiGjGVQsem2cxoIX61uRGy+Jux2s9C/kKbTrWLdrU54= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6/go.mod h1:SSPEdf9spsFgJyhjrXvawfpyzrXHBCUe+2eQ1CjC1Ak= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 h1:bt3zw79tm209glISdMRCIVRCwvSDXxgAxh5KWe2qHkY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0/go.mod h1:viTrxhAuejD+LszDahzAE2x40YjYWhMqzHxv2ZiWaME= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.13.0 h1:Xlmdkxi8WcIwX5Cy9BS+scWcmvARw8pg0bi7kaeERUY= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.13.0/go.mod h1:eNvoR4P1XQN7xElmYA8cWeFENLY3pfsj/5nFRItzXnA= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.11.0 h1:QN/wfWh/FJud6IKobe7QUMw1J0NfdZVtqvndyFgofCg= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.11.0/go.mod h1:tS6jI0oPA0cVqUdZJe0qea1u7YnCejeTi4o6rAk9VO0= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.29.0 h1:7jk4NfzDnnSbaR9E4mOBWRZXQThq5rsqjlDC+uu9dsI= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.29.0/go.mod h1:HoTu0hnXGafTpKIZQ60jw0ybhhCH1QYf20oL7GEJFdg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0 h1:F1diQIOkNn8jcez4173r+PLPdkWK7chy74r3fKpDrLI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0/go.mod h1:8ctElVINyp+SjhoZZceUAZw78glZH6R8ox5MVNu5j2s= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.5.0 h1:tzVhIPr/psp8Gb2Blst9mq6HklkhAGPqv2eaiSq6yoU= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.5.0/go.mod h1:u0rI/Mm45zCJe86J5kvPfG7pYzkVZzNjEkoTVbfOYE8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU= +github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA= +github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU= +github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E= +github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk= +github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.11.1 h1:IQ+lPZVkSM3FRtyaDox41R8YS6iwPMYIreejOgPW49g= +github.com/aws/smithy-go v1.11.1/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= +github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/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/cespare/xxhash/v2 v2.1.2/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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/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/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +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/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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/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 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= +github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +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-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +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/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc= +github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +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.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jsonmaur/aws-regions/v2 v2.3.1 h1:WWt452LyhjI4ZCRKBSULVHqIGE8/9UqVQOSAzuc2woE= +github.com/jsonmaur/aws-regions/v2 v2.3.1/go.mod h1:NqtmZ2wG5HkrTYFQ+II3BDysj0yek59yjtZjAaCn8lE= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jwalton/gchalk v1.3.0 h1:uTfAaNexN8r0I9bioRTksuT8VGjrPs9YIXR1PQbtX/Q= +github.com/jwalton/gchalk v1.3.0/go.mod h1:ytRlj60R9f7r53IAElbpq4lVuPOPNg2J4tJcCxtFqr8= +github.com/jwalton/go-supportscolor v1.1.0 h1:HsXFJdMPjRUAx8cIW6g30hVSFYaxh9yRQwEWgkAR7lQ= +github.com/jwalton/go-supportscolor v1.1.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= +github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o= +github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= +github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= +github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= +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= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +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/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000 h1:SL+8VVnkqyshUSz5iNnXtrBQzvFF2SkROm6t5RczFAE= +golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/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-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-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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-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-20211005180243-6b3c2da341f1/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-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/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-20210816183151-1e6c022a8912/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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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-20211205182925-97ca703d548d/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-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= +golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/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-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/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-20190422233926-fe54fb35175b/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-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +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.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.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-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-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/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-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/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-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +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.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= +google.golang.org/grpc v1.46.2/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 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..d1ad13a --- /dev/null +++ b/install.sh @@ -0,0 +1,381 @@ +#!/bin/sh +set -e +# Code generated by godownloader on 2022-05-30T14:47:28Z. DO NOT EDIT. +# + +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +log_prefix() { + echo "$0" +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo ${arch} +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} +hash_sha256_verify() { + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +} +cat /dev/null < 0 { + bold := constants.Bold + fmt.Println(bold("[" + startDevEnvReply.LogLineHeader + "]\n")) + } + + if len(startDevEnvReply.LogLine) > 0 { + log.Println(startDevEnvReply.LogLine) + } + } + + return nil +} diff --git a/internal/aws/errors_presenter.go b/internal/aws/errors_presenter.go new file mode 100644 index 0000000..6387b6f --- /dev/null +++ b/internal/aws/errors_presenter.go @@ -0,0 +1,157 @@ +package aws + +import ( + "errors" + "fmt" + + "github.com/recode-sh/aws-cloud-provider/config" + "github.com/recode-sh/aws-cloud-provider/service" + "github.com/recode-sh/aws-cloud-provider/userconfig" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/recode/entities" +) + +type AWSViewableErrorBuilder struct { + presenters.RecodeViewableErrorBuilder +} + +func NewAWSViewableErrorBuilder() AWSViewableErrorBuilder { + return AWSViewableErrorBuilder{} +} + +func (v AWSViewableErrorBuilder) Build(err error) (viewableError *presenters.ViewableError) { + viewableError = &presenters.ViewableError{} + + if errors.Is(err, entities.ErrRecodeNotInstalled) { + viewableError.Title = "Recode not installed" + viewableError.Message = "Recode is not installed in this region on this AWS account.\n\n" + + "Please double check the passed credentials and region." + + return + } + + if errors.Is(err, entities.ErrUninstallExistingDevEnvs) { + viewableError.Title = "Existing development environments" + viewableError.Message = "All development environments need to be removed before uninstalling Recode." + + return + } + + if errors.Is(err, userconfig.ErrMissingConfig) { + viewableError.Title = "No AWS account found" + viewableError.Message = fmt.Sprintf(`An AWS account can be configured: + + - by setting the \"%s\" and \"%s\" environment variables. + + - by installing the AWS CLI and running \"aws configure\".`, + userconfig.AWSAccessKeyIDEnvVar, + userconfig.AWSSecretAccessKeyEnvVar) + + return + } + + if errors.Is(err, userconfig.ErrMissingAccessKeyInEnv) { + viewableError.Title = "Missing environment variable" + viewableError.Message = fmt.Sprintf( + "The environment variable \"%s\" needs to be set.", + userconfig.AWSAccessKeyIDEnvVar, + ) + + return + } + + if errors.Is(err, userconfig.ErrMissingSecretInEnv) { + viewableError.Title = "Missing environment variable" + viewableError.Message = fmt.Sprintf( + "The environment variable \"%s\" needs to be set.", + userconfig.AWSSecretAccessKeyEnvVar, + ) + + return + } + + if errors.Is(err, userconfig.ErrMissingRegionInEnv) { + viewableError.Title = "Missing region" + viewableError.Message = fmt.Sprintf( + "A region needs to be specified by setting the environment variable \"%s\" or by using the flag \"--region\".", + userconfig.AWSRegionEnvVar, + ) + + return + } + + if errors.Is(err, userconfig.ErrMissingRegionInFiles) { + viewableError.Title = "Missing region" + viewableError.Message = "A region needs to be specified by using the flag \"--region\"." + + return + } + + if typedError, ok := err.(userconfig.ErrProfileNotFound); ok { + viewableError.Title = "Configuration profile not found" + viewableError.Message = fmt.Sprintf( + "The profile \"%s\" was not found in your AWS configuration.\n\n(Searched in \"%s\" and \"%s\").", + typedError.Profile, + typedError.CredentialsFilePath, + typedError.ConfigFilePath, + ) + + return + } + + if typedError, ok := err.(config.ErrInvalidRegion); ok { + viewableError.Title = "Invalid region" + viewableError.Message = fmt.Sprintf( + "The region \"%s\" is invalid.", + typedError.Region, + ) + + return + } + + if typedError, ok := err.(config.ErrInvalidAccessKeyID); ok { + viewableError.Title = "Invalid access key ID" + viewableError.Message = fmt.Sprintf( + "The access key ID \"%s\" is invalid.", + typedError.AccessKeyID, + ) + + return + } + + if typedError, ok := err.(config.ErrInvalidSecretAccessKey); ok { + viewableError.Title = "Invalid secret access key" + viewableError.Message = fmt.Sprintf( + "The secret access key \"%s\" is invalid.", + typedError.SecretAccessKey, + ) + + return + } + + if typedError, ok := err.(service.ErrInvalidInstanceType); ok { + viewableError.Title = "Invalid instance type" + viewableError.Message = fmt.Sprintf( + "The instance type \"%s\" is invalid in the region \"%s\".", + typedError.InstanceType, + typedError.Region, + ) + + return + } + + if typedError, ok := err.(service.ErrInvalidInstanceTypeArch); ok { + viewableError.Title = "Unsupported instance type" + viewableError.Message = fmt.Sprintf( + "The instance type \"%s\" is not supported by Recode.\n\n"+ + "Only on-demand instances with EBS and architectures \"%s\" are supported.", + typedError.InstanceType, + typedError.SupportedArchs, + ) + + return + } + + viewableError = v.RecodeViewableErrorBuilder.Build(err) + return +} diff --git a/internal/aws/user_config_local_resolver.go b/internal/aws/user_config_local_resolver.go new file mode 100644 index 0000000..5a0c27f --- /dev/null +++ b/internal/aws/user_config_local_resolver.go @@ -0,0 +1,81 @@ +package aws + +import ( + "errors" + + "github.com/recode-sh/aws-cloud-provider/userconfig" +) + +type UserConfigLocalResolverOpts struct { + Profile string +} + +//go:generate mockgen -destination=../mocks/aws_user_config_env_vars_resolver.go -package=mocks -mock_names UserConfigEnvVarsResolver=AWSUserConfigEnvVarsResolver github.com/recode-sh/cli/internal/aws UserConfigEnvVarsResolver +type UserConfigEnvVarsResolver interface { + Resolve() (*userconfig.Config, error) +} + +//go:generate mockgen -destination=../mocks/aws_user_config_files_resolver.go -package=mocks -mock_names UserConfigFilesResolver=AWSUserConfigFilesResolver github.com/recode-sh/cli/internal/aws UserConfigFilesResolver +type UserConfigFilesResolver interface { + Resolve() (*userconfig.Config, error) +} + +// UserConfigLocalResolver represents the default implementation +// of the UserConfigResolver interface, used by most AWS commands via +// the SDKConfigStaticBuilder. +// +// It retrieves the AWS account configuration from environment variables +// (via the UserConfigLocalEnvVarsResolver interface) and fallback to config +// files (via the UserConfigLocalFilesResolver interface) otherwise. +// +type UserConfigLocalResolver struct { + envVarsResolver UserConfigEnvVarsResolver + configFilesResolver UserConfigFilesResolver + opts UserConfigLocalResolverOpts +} + +// NewUserConfigLocalResolver constructs +// the UserConfigLocalResolver struct. +// Used by Wire in dependencies. +// +func NewUserConfigLocalResolver( + envVarsResolver UserConfigEnvVarsResolver, + configFilesResolver UserConfigFilesResolver, + opts UserConfigLocalResolverOpts, +) UserConfigLocalResolver { + + return UserConfigLocalResolver{ + envVarsResolver: envVarsResolver, + configFilesResolver: configFilesResolver, + opts: opts, + } +} + +// Resolve retrieves the AWS account configuration from environment variables +// and fallback to config files if no environment variables were found. +// +// If the Profile option is set, environment variables are ignored +// and the profile is directly loaded from config files. +// +func (u UserConfigLocalResolver) Resolve() (*userconfig.Config, error) { + var userConfig *userconfig.Config + var err error + + if len(u.opts.Profile) == 0 { + userConfig, err = u.envVarsResolver.Resolve() + + if err != nil && !errors.Is(err, userconfig.ErrMissingConfig) { + return nil, err + } + } + + if userConfig == nil { + userConfig, err = u.configFilesResolver.Resolve() + + if err != nil { + return nil, err + } + } + + return userConfig, nil +} diff --git a/internal/aws/user_config_local_resolver_test.go b/internal/aws/user_config_local_resolver_test.go new file mode 100644 index 0000000..0f2f71f --- /dev/null +++ b/internal/aws/user_config_local_resolver_test.go @@ -0,0 +1,134 @@ +package aws + +import ( + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/recode-sh/aws-cloud-provider/userconfig" + "github.com/recode-sh/cli/internal/mocks" +) + +func TestUserConfigLocalResolving(t *testing.T) { + testCases := []struct { + test string + configInEnvVars *userconfig.Config + errorDuringEnvVarsResolving error + configInFiles *userconfig.Config + errorDuringConfigFilesResolving error + profileOpts string + expectedConfig *userconfig.Config + expectedError error + }{ + { + test: "no env vars, no config files", + errorDuringEnvVarsResolving: userconfig.ErrMissingConfig, + errorDuringConfigFilesResolving: userconfig.ErrMissingConfig, + expectedConfig: nil, + expectedError: userconfig.ErrMissingConfig, + }, + + { + test: "only env vars", + configInEnvVars: userconfig.NewConfig("a", "b", "c"), + errorDuringConfigFilesResolving: userconfig.ErrMissingConfig, + expectedConfig: userconfig.NewConfig("a", "b", "c"), + expectedError: nil, + }, + + { + test: "only config files", + errorDuringEnvVarsResolving: userconfig.ErrMissingConfig, + configInFiles: userconfig.NewConfig("a", "b", "c"), + expectedConfig: userconfig.NewConfig("a", "b", "c"), + expectedError: nil, + }, + + { + test: "env vars and config files", + configInEnvVars: userconfig.NewConfig("a", "b", "c"), + configInFiles: userconfig.NewConfig("d", "e", "f"), + expectedConfig: userconfig.NewConfig("a", "b", "c"), + expectedError: nil, + }, + + { + test: "env vars, config files and profile", + configInEnvVars: userconfig.NewConfig("a", "b", "c"), + configInFiles: userconfig.NewConfig("d", "e", "f"), + profileOpts: "production", + expectedConfig: userconfig.NewConfig("d", "e", "f"), + expectedError: nil, + }, + + { + test: "errored env vars and config files", + errorDuringEnvVarsResolving: userconfig.ErrMissingAccessKeyInEnv, + configInFiles: userconfig.NewConfig("d", "e", "f"), + expectedConfig: nil, + expectedError: userconfig.ErrMissingAccessKeyInEnv, + }, + + { + test: "env vars and errored config files", + configInEnvVars: userconfig.NewConfig("a", "b", "c"), + errorDuringConfigFilesResolving: userconfig.ErrMissingRegionInFiles, + expectedConfig: userconfig.NewConfig("a", "b", "c"), + expectedError: nil, + }, + + { + test: "env vars, errored config files and profile", + configInEnvVars: userconfig.NewConfig("a", "b", "c"), + errorDuringConfigFilesResolving: userconfig.ErrMissingRegionInFiles, + profileOpts: "production", + expectedConfig: nil, + expectedError: userconfig.ErrMissingRegionInFiles, + }, + } + + for _, tc := range testCases { + t.Run(tc.test, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + userConfigEnvVarsResolverMock := mocks.NewAWSUserConfigEnvVarsResolver(mockCtrl) + userConfigEnvVarsResolverMock.EXPECT().Resolve().Return( + tc.configInEnvVars, + tc.errorDuringEnvVarsResolving, + ).AnyTimes() + + userConfigFilesResolverMock := mocks.NewAWSUserConfigFilesResolver(mockCtrl) + userConfigFilesResolverMock.EXPECT().Resolve().Return( + tc.configInFiles, + tc.errorDuringConfigFilesResolving, + ).AnyTimes() + + resolver := NewUserConfigLocalResolver( + userConfigEnvVarsResolverMock, + userConfigFilesResolverMock, + UserConfigLocalResolverOpts{ + Profile: tc.profileOpts, + }, + ) + + resolvedConfig, err := resolver.Resolve() + + if tc.expectedError == nil && err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { + t.Fatalf("expected error to equal '%+v', got '%+v'", tc.expectedError, err) + } + + if tc.expectedConfig != nil && *resolvedConfig != *tc.expectedConfig { + t.Fatalf("expected config to equal '%+v', got '%+v'", *tc.expectedConfig, *resolvedConfig) + } + + if tc.expectedConfig == nil && resolvedConfig != nil { + t.Fatalf("expected no config, got '%+v'", *resolvedConfig) + } + }) + } +} diff --git a/internal/cmd/aws.go b/internal/cmd/aws.go new file mode 100644 index 0000000..7b1cc1b --- /dev/null +++ b/internal/cmd/aws.go @@ -0,0 +1,79 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "github.com/aws/aws-sdk-go-v2/config" + "github.com/spf13/cobra" +) + +var awsProfile string +var awsRegion string + +var awsCredentialsFilePath string +var awsConfigFilePath string + +// awsCmd represents the aws command +var awsCmd = &cobra.Command{ + Use: "aws", + + Short: "Use Recode on Amazon Web Services", + + Long: `Use Recode on Amazon Web Services. + +To begin, create your first development environment using the command: + + recode aws start + +Once started, you could stop it at any time, to save costs, using the command: + + recode aws stop + +If you don't plan to use this development environment again, you could remove it using the command: + + recode aws remove `, + + Example: ` recode aws start recode-sh/api --instance-type m4.large + recode aws stop recode-sh/api + recode aws remove recode-sh/api`, +} + +func init() { + awsCmd.Flags().StringVar( + &awsProfile, + "profile", + "", + "the configuration profile to use to access your AWS account", + ) + + awsCmd.Flags().StringVar( + &awsRegion, + "region", + "", + "the region to use to access your AWS account", + ) + + awsCredentialsFilePath = config.DefaultSharedCredentialsFilename() + awsConfigFilePath = config.DefaultSharedConfigFilename() + + rootCmd.AddCommand(awsCmd) +} diff --git a/internal/cmd/aws_remove.go b/internal/cmd/aws_remove.go new file mode 100644 index 0000000..e63b648 --- /dev/null +++ b/internal/cmd/aws_remove.go @@ -0,0 +1,114 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "os" + + "github.com/recode-sh/cli/internal/dependencies" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/recode/features" + "github.com/spf13/cobra" +) + +var awsRemoveForceDevEnvRemove bool + +// awsRemoveCmd represents the aws remove command +var awsRemoveCmd = &cobra.Command{ + Use: "remove (|)", + + Short: "Remove a development environment", + + Long: `Remove an existing development environment. + +The development environment will be PERMANENTLY removed along with all its data. + +There is no going back, so please be sure to save your work before running this command.`, + + Example: ` recode aws remove api + recode aws remove recode-sh/cli --force`, + + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + + recodeViewableErrorBuilder := dependencies.ProvideRecodeViewableErrorBuilder() + baseView := dependencies.ProvideBaseView() + + repository := args[0] + checkForRepositoryExistence := false + + repositoryResolver := dependencies.ProvideDevEnvRepositoryResolver() + resolvedRepository, err := repositoryResolver.Resolve( + repository, + checkForRepositoryExistence, + ) + + if err != nil { + baseView.ShowErrorViewWithStartingNewLine( + recodeViewableErrorBuilder.Build( + err, + ), + ) + + os.Exit(1) + } + + awsRemoveInput := features.RemoveInput{ + ResolvedRepository: *resolvedRepository, + PreRemoveHook: dependencies.ProvidePreRemoveHook(), + ForceRemove: awsRemoveForceDevEnvRemove, + ConfirmRemove: func() (bool, error) { + logger := system.NewLogger() + return system.AskForConfirmation( + logger, + os.Stdin, + "All your un-pushed work will be lost.", + ) + }, + } + + awsRemove := dependencies.ProvideAWSRemoveFeature( + awsRegion, + awsProfile, + awsCredentialsFilePath, + awsConfigFilePath, + ) + + err = awsRemove.Execute(awsRemoveInput) + + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + awsRemoveCmd.Flags().BoolVar( + &awsRemoveForceDevEnvRemove, + "force", + false, + "avoid remove confirmation", + ) + + awsCmd.AddCommand(awsRemoveCmd) +} diff --git a/internal/cmd/aws_start.go b/internal/cmd/aws_start.go new file mode 100644 index 0000000..afa04d6 --- /dev/null +++ b/internal/cmd/aws_start.go @@ -0,0 +1,143 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "os" + + "github.com/recode-sh/cli/internal/dependencies" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/recode/features" + "github.com/spf13/cobra" +) + +var awsStartInstanceType string +var awsStartDevEnvRebuildAsked bool +var awsStartForceDevEnvRebuild bool + +// awsStartCmd represents the aws start command +var awsStartCmd = &cobra.Command{ + Use: "start (|)", + + Short: "Start a development environment", + + Long: `Start a development environment for a specific GitHub repository. + +If the passed repository doesn't contain an account name, your personal account is assumed.`, + + Example: ` recode aws start api + recode aws start recode-sh/cli --instance-type m4.large`, + + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + + recodeViewableErrorBuilder := dependencies.ProvideRecodeViewableErrorBuilder() + baseView := dependencies.ProvideBaseView() + + devEnvUserConfigResolver := dependencies.ProvideDevEnvUserConfigResolver() + resolvedDevEnvUserConfig, err := devEnvUserConfigResolver.Resolve() + + if err != nil { + baseView.ShowErrorViewWithStartingNewLine( + recodeViewableErrorBuilder.Build( + err, + ), + ) + + os.Exit(1) + } + + repository := args[0] + checkForRepositoryExistence := true + + repositoryResolver := dependencies.ProvideDevEnvRepositoryResolver() + resolvedRepository, err := repositoryResolver.Resolve( + repository, + checkForRepositoryExistence, + ) + + if err != nil { + baseView.ShowErrorViewWithStartingNewLine( + recodeViewableErrorBuilder.Build( + err, + ), + ) + + os.Exit(1) + } + + awsStartInput := features.StartInput{ + InstanceType: awsStartInstanceType, + DevEnvRebuildAsked: awsStartDevEnvRebuildAsked, + ResolvedDevEnvUserConfig: *resolvedDevEnvUserConfig, + ResolvedRepository: *resolvedRepository, + ForceDevEnvRevuild: awsStartForceDevEnvRebuild, + ConfirmDevEnvRebuild: func() (bool, error) { + logger := system.NewLogger() + return system.AskForConfirmation( + logger, + os.Stdin, + "All your un-pushed work will be lost", + ) + }, + } + + awsStart := dependencies.ProvideAWSStartFeature( + awsRegion, + awsProfile, + awsCredentialsFilePath, + awsConfigFilePath, + ) + + err = awsStart.Execute(awsStartInput) + + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + awsStartCmd.Flags().StringVar( + &awsStartInstanceType, + "instance-type", + "t2.medium", + "the instance type used by this development environment", + ) + + awsStartCmd.Flags().BoolVar( + &awsStartDevEnvRebuildAsked, + "rebuild", + false, + "rebuild the development environment", + ) + + awsStartCmd.Flags().BoolVar( + &awsStartForceDevEnvRebuild, + "force", + false, + "avoid rebuild confirmation", + ) + + awsCmd.AddCommand(awsStartCmd) +} diff --git a/internal/cmd/aws_stop.go b/internal/cmd/aws_stop.go new file mode 100644 index 0000000..2beba9c --- /dev/null +++ b/internal/cmd/aws_stop.go @@ -0,0 +1,95 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "os" + + "github.com/recode-sh/cli/internal/dependencies" + "github.com/recode-sh/recode/features" + "github.com/spf13/cobra" +) + +// awsStopCmd represents the aws stop command +var awsStopCmd = &cobra.Command{ + Use: "stop (|)", + + Short: "Stop a development environment", + + Long: `Stop an existing development environment. + +The development environment will be stopped but your data will be conserved. + +You may still incur charges for the storage used. If you don't plan to use this development environment again, use the remove command instead.`, + + Example: ` recode aws stop api + recode aws stop recode-sh/cli`, + + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + + recodeViewableErrorBuilder := dependencies.ProvideRecodeViewableErrorBuilder() + baseView := dependencies.ProvideBaseView() + + repository := args[0] + checkForRepositoryExistence := false + + repositoryResolver := dependencies.ProvideDevEnvRepositoryResolver() + resolvedRepository, err := repositoryResolver.Resolve( + repository, + checkForRepositoryExistence, + ) + + if err != nil { + baseView.ShowErrorViewWithStartingNewLine( + recodeViewableErrorBuilder.Build( + err, + ), + ) + + os.Exit(1) + } + + awsStopInput := features.StopInput{ + ResolvedRepository: *resolvedRepository, + PreStopHook: dependencies.ProvidePreStopHook(), + } + + awsStop := dependencies.ProvideAWSStopFeature( + awsRegion, + awsProfile, + awsCredentialsFilePath, + awsConfigFilePath, + ) + + err = awsStop.Execute(awsStopInput) + + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + awsCmd.AddCommand(awsStopCmd) +} diff --git a/internal/cmd/aws_uninstall.go b/internal/cmd/aws_uninstall.go new file mode 100644 index 0000000..52cac5d --- /dev/null +++ b/internal/cmd/aws_uninstall.go @@ -0,0 +1,68 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "os" + + "github.com/recode-sh/cli/internal/dependencies" + "github.com/recode-sh/recode/features" + "github.com/spf13/cobra" +) + +// awsUninstallCmd represents the aws uninstall command +var awsUninstallCmd = &cobra.Command{ + Use: "uninstall", + + Short: "Uninstall Recode from your AWS account", + + Long: `Uninstall Recode from your AWS account. + +All your development environments must be removed before running this command.`, + + Example: " recode aws uninstall", + + Run: func(cmd *cobra.Command, args []string) { + + awsUninstallInput := features.UninstallInput{ + SuccessMessage: "Recode has been uninstalled from this region on this AWS account.", + AlreadyUninstalledMessage: "Recode is already uninstalled in this region on this AWS account.", + } + + awsUninstall := dependencies.ProvideAWSUninstallFeature( + awsRegion, + awsProfile, + awsCredentialsFilePath, + awsConfigFilePath, + ) + + err := awsUninstall.Execute(awsUninstallInput) + + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + awsCmd.AddCommand(awsUninstallCmd) +} diff --git a/internal/cmd/login.go b/internal/cmd/login.go new file mode 100644 index 0000000..38528a1 --- /dev/null +++ b/internal/cmd/login.go @@ -0,0 +1,65 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "os" + + "github.com/recode-sh/cli/internal/dependencies" + "github.com/recode-sh/cli/internal/features" + "github.com/spf13/cobra" +) + +// loginCmd represents the "recode login" command +var loginCmd = &cobra.Command{ + Use: "login", + + Short: "Connect a GitHub account to use with Recode", + + Long: `Connect a GitHub account to use with Recode. + +Recode requires the following permissions: + + - "Public SSH keys" and "Repositories" to let you access your repositories from your development environments + + - "GPG Keys" and "Personal user data" to configure Git and sign your commits (verified badge) + +All your data (including the OAuth access token) will only be stored locally.`, + + Example: " recode login", + + Run: func(cmd *cobra.Command, args []string) { + loginInput := features.LoginInput{} + + login := dependencies.ProvideLoginFeature() + + err := login.Execute(loginInput) + + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + rootCmd.AddCommand(loginCmd) +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..0abafed --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,218 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + "io/fs" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/cli/internal/dependencies" + "github.com/recode-sh/cli/internal/exceptions" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/cli/internal/vscode" + "github.com/recode-sh/recode/github" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "recode", + + Short: "Remote development environments defined as code", + + Long: `Recode - Remote development environments defined as code + +To begin, run the command "recode login" to connect your GitHub account. + +From there, the most common workflow is: + + - recode start : to start a development environment for a specific GitHub repository + - recode stop : to stop a development environment (without removing your data) + - recode remove : to remove a development environment AND your data + + may be relative to your personal GitHub account (eg: cli) or fully qualified (eg: my-organization/api).`, + + PersistentPreRun: func(cmd *cobra.Command, args []string) { + ensureUserIsLoggedIn(cmd) + }, + + TraverseChildren: true, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize( + ensureRecodeCLIRequirements, + initializeRecodeCLIConfig, + ensureGitHubAccessTokenValidity, + ) +} + +func ensureRecodeCLIRequirements() { + missingRequirements := []string{} + + vscodeCLI := vscode.CLI{} + _, err := vscodeCLI.LookupPath(runtime.GOOS) + + if vscodeCLINotFoundErr, ok := err.(vscode.ErrCLINotFound); ok { + missingRequirements = append( + missingRequirements, + fmt.Sprintf( + "Visual Studio Code (looked in \"%s)", + strings.Join(vscodeCLINotFoundErr.VisitedPaths, "\", \"")+"\"", + ), + ) + } + + sshCommand := "ssh" + _, err = exec.LookPath(sshCommand) + + if err != nil { + missingRequirements = append( + missingRequirements, + fmt.Sprintf( + "OpenSSH client (looked for an \"%s\" command available)", + sshCommand, + ), + ) + } + + if len(missingRequirements) > 0 { + recodeViewableErrorBuilder := dependencies.ProvideRecodeViewableErrorBuilder() + baseView := dependencies.ProvideBaseView() + + missingRequirementsErr := exceptions.ErrMissingRequirements{ + MissingRequirements: missingRequirements, + } + + baseView.ShowErrorViewWithStartingNewLine( + recodeViewableErrorBuilder.Build( + missingRequirementsErr, + ), + ) + + os.Exit(1) + } +} + +func initializeRecodeCLIConfig() { + configDir := system.UserConfigDir() + configDirPerms := fs.FileMode(0700) + + // Ensure configuration dir exists + err := os.MkdirAll( + configDir, + configDirPerms, + ) + cobra.CheckErr(err) + + configFilePath := system.UserConfigFilePath() + configFilePerms := fs.FileMode(0600) + + // Ensure configuration file exists + f, err := os.OpenFile( + configFilePath, + os.O_CREATE, + configFilePerms, + ) + cobra.CheckErr(err) + defer f.Close() + + viper.SetConfigFile(configFilePath) + cobra.CheckErr(viper.ReadInConfig()) +} + +// ensureGitHubAccessTokenValidity ensures that +// the github access token has not been +// revoked by user +func ensureGitHubAccessTokenValidity() { + userConfig := config.NewUserConfig() + userIsLoggedIn := userConfig.GetBool(config.UserConfigKeyUserIsLoggedIn) + + if !userIsLoggedIn { + return + } + + gitHubService := github.NewService() + + githubUser, err := gitHubService.GetAuthenticatedUser( + userConfig.GetString( + config.UserConfigKeyGitHubAccessToken, + ), + ) + + if err != nil && + gitHubService.IsInvalidAccessTokenError(err) { // User has revoked access token + + userIsLoggedIn = false + + userConfig.Set( + config.UserConfigKeyUserIsLoggedIn, + userIsLoggedIn, + ) + + // Error is swallowed here to + // not confuse user with unexpected error + _ = userConfig.WriteConfig() + } + + if err == nil { + // Update config with updated values from GitHub + userConfig.PopulateFromGitHubUser(githubUser) + + // Error is swallowed here to + // not confuse user with unexpected error + _ = userConfig.WriteConfig() + } +} + +func ensureUserIsLoggedIn(cmd *cobra.Command) { + userConfig := config.NewUserConfig() + userIsLoggedIn := userConfig.GetBool(config.UserConfigKeyUserIsLoggedIn) + + if !userIsLoggedIn && cmd != loginCmd { + recodeViewableErrorBuilder := dependencies.ProvideRecodeViewableErrorBuilder() + baseView := dependencies.ProvideBaseView() + + baseView.ShowErrorViewWithStartingNewLine( + recodeViewableErrorBuilder.Build( + exceptions.ErrUserNotLoggedIn, + ), + ) + + os.Exit(1) + } +} diff --git a/internal/config/github.go b/internal/config/github.go new file mode 100644 index 0000000..9242cf5 --- /dev/null +++ b/internal/config/github.go @@ -0,0 +1,16 @@ +package config + +var ( + GitHubOAuthClientID = "a8c368bfe297f0b1808a" + GitHubOAuthCLIToAPIURL = "http://127.0.0.1:8080/github/oauth/callback" + + GitHubOAuthAPIToCLIURLPath = "/github/oauth/callback" + + GitHubOAuthScopes = []string{ + "read:user", + "user:email", + "repo", + "admin:public_key", + "admin:gpg_key", + } +) diff --git a/internal/config/github_prod.go b/internal/config/github_prod.go new file mode 100644 index 0000000..d85a6f7 --- /dev/null +++ b/internal/config/github_prod.go @@ -0,0 +1,8 @@ +//go:build prod + +package config + +func init() { + GitHubOAuthClientID = "7e1b6c93f4ba81819162" + GitHubOAuthCLIToAPIURL = "https://recode-sh-api.herokuapp.com/github/oauth/callback" +} diff --git a/internal/config/user_config.go b/internal/config/user_config.go new file mode 100644 index 0000000..41b9dd0 --- /dev/null +++ b/internal/config/user_config.go @@ -0,0 +1,55 @@ +package config + +import ( + "github.com/recode-sh/recode/github" + "github.com/spf13/viper" +) + +type UserConfigKey string + +const ( + UserConfigKeyUserIsLoggedIn UserConfigKey = "user_is_logged_in" + UserConfigKeyGitHubAccessToken UserConfigKey = "github_access_token" + UserConfigKeyGitHubUsername UserConfigKey = "github_username" + UserConfigKeyGitHubEmail UserConfigKey = "github_email" + UserConfigKeyGitHubFullName UserConfigKey = "github_full_name" +) + +type UserConfig struct{} + +func NewUserConfig() UserConfig { + return UserConfig{} +} + +func (UserConfig) GetString(key UserConfigKey) string { + return viper.GetString(string(key)) +} + +func (UserConfig) GetBool(key UserConfigKey) bool { + return viper.GetBool(string(key)) +} + +func (UserConfig) Set(key UserConfigKey, value interface{}) { + viper.Set(string(key), value) +} + +func (u UserConfig) PopulateFromGitHubUser(githubUser *github.AuthenticatedUser) { + u.Set( + UserConfigKeyGitHubEmail, + githubUser.PrimaryEmail, + ) + + u.Set( + UserConfigKeyGitHubFullName, + githubUser.FullName, + ) + + u.Set( + UserConfigKeyGitHubUsername, + githubUser.Username, + ) +} + +func (UserConfig) WriteConfig() error { + return viper.WriteConfig() +} diff --git a/internal/constants/colors.go b/internal/constants/colors.go new file mode 100644 index 0000000..f9dca78 --- /dev/null +++ b/internal/constants/colors.go @@ -0,0 +1,13 @@ +package constants + +import "github.com/jwalton/gchalk" + +var ( + Underline = gchalk.Underline + Bold = gchalk.Bold + Green = gchalk.RGB(175, 202, 90) + Blue = gchalk.RGB(130, 189, 237) + Cyan = gchalk.RGB(125, 192, 203) + Red = gchalk.RGB(218, 136, 138) + Yellow = gchalk.RGB(241, 199, 141) +) diff --git a/internal/dependencies/aws_remove.go b/internal/dependencies/aws_remove.go new file mode 100644 index 0000000..b9d114f --- /dev/null +++ b/internal/dependencies/aws_remove.go @@ -0,0 +1,61 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + awsProviderUserConfig "github.com/recode-sh/aws-cloud-provider/userconfig" + awsCLI "github.com/recode-sh/cli/internal/aws" + featuresCLI "github.com/recode-sh/cli/internal/features" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/views" + "github.com/recode-sh/recode/features" +) + +func ProvideAWSRemoveFeature(region, profile, credentialsFilePath, configFilePath string) features.RemoveFeature { + return provideAWSRemoveFeature( + awsProviderUserConfig.EnvVarsResolverOpts{ + Region: region, + }, + + awsProviderUserConfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, + + awsCLI.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +func provideAWSRemoveFeature( + userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, + userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, + userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, +) features.RemoveFeature { + panic( + wire.Build( + viewSet, + awsServiceBuilderSet, + awsViewableErrorBuilder, + + stepperSet, + + wire.Bind(new(features.RemoveOutputHandler), new(featuresCLI.RemoveOutputHandler)), + featuresCLI.NewRemoveOutputHandler, + + wire.Bind(new(featuresCLI.RemovePresenter), new(presenters.RemovePresenter)), + presenters.NewRemovePresenter, + + wire.Bind(new(presenters.RemoveViewer), new(views.RemoveView)), + views.NewRemoveView, + + features.NewRemoveFeature, + ), + ) +} diff --git a/internal/dependencies/aws_shared.go b/internal/dependencies/aws_shared.go new file mode 100644 index 0000000..85cc66e --- /dev/null +++ b/internal/dependencies/aws_shared.go @@ -0,0 +1,47 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + awsProviderConfig "github.com/recode-sh/aws-cloud-provider/config" + awsProviderService "github.com/recode-sh/aws-cloud-provider/service" + awsProviderUserConfig "github.com/recode-sh/aws-cloud-provider/userconfig" + awsCLI "github.com/recode-sh/cli/internal/aws" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/recode/entities" +) + +var awsViewableErrorBuilder = wire.NewSet( + wire.Bind(new(presenters.ViewableErrorBuilder), new(awsCLI.AWSViewableErrorBuilder)), + awsCLI.NewAWSViewableErrorBuilder, +) + +var awsServiceBuilderSet = wire.NewSet( + wire.Bind(new(awsProviderUserConfig.ProfileLoader), new(awsProviderConfig.ProfileLoader)), + awsProviderConfig.NewProfileLoader, + + wire.Bind(new(awsCLI.UserConfigFilesResolver), new(awsProviderUserConfig.FilesResolver)), + awsProviderUserConfig.NewFilesResolver, + + wire.Bind(new(awsProviderUserConfig.EnvVarsGetter), new(system.EnvVars)), + system.NewEnvVars, + + wire.Bind(new(awsCLI.UserConfigEnvVarsResolver), new(awsProviderUserConfig.EnvVarsResolver)), + awsProviderUserConfig.NewEnvVarsResolver, + + wire.Bind(new(awsProviderService.UserConfigResolver), new(awsCLI.UserConfigLocalResolver)), + awsCLI.NewUserConfigLocalResolver, + + wire.Bind(new(awsProviderService.UserConfigValidator), new(awsProviderConfig.UserConfigValidator)), + awsProviderConfig.NewUserConfigValidator, + + wire.Bind(new(awsProviderService.UserConfigLoader), new(awsProviderConfig.UserConfigLoader)), + awsProviderConfig.NewUserConfigLoader, + + wire.Bind(new(entities.CloudServiceBuilder), new(awsProviderService.Builder)), + awsProviderService.NewBuilder, +) diff --git a/internal/dependencies/aws_start.go b/internal/dependencies/aws_start.go new file mode 100644 index 0000000..ec10fc4 --- /dev/null +++ b/internal/dependencies/aws_start.go @@ -0,0 +1,81 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + awsProviderUserConfig "github.com/recode-sh/aws-cloud-provider/userconfig" + "github.com/recode-sh/cli/internal/agent" + awsCLI "github.com/recode-sh/cli/internal/aws" + featuresCLI "github.com/recode-sh/cli/internal/features" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/views" + "github.com/recode-sh/recode/features" +) + +func ProvideAWSStartFeature(region, profile, credentialsFilePath, configFilePath string) features.StartFeature { + return provideAWSStartFeature( + awsProviderUserConfig.EnvVarsResolverOpts{ + Region: region, + }, + + awsProviderUserConfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, + + awsCLI.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +func provideAWSStartFeature( + userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, + userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, + userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, +) features.StartFeature { + panic( + wire.Build( + viewSet, + awsServiceBuilderSet, + awsViewableErrorBuilder, + + userConfigManagerSet, + + wire.Bind(new(agent.ClientBuilder), new(agent.DefaultClientBuilder)), + agent.NewDefaultClientBuilder, + + githubManagerSet, + + loggerSet, + + sshConfigManagerSet, + + sshKnownHostsManagerSet, + + sshKeysManagerSet, + + vscodeProcessManagerSet, + + vscodeExtensionsManagerSet, + + stepperSet, + + wire.Bind(new(features.StartOutputHandler), new(featuresCLI.StartOutputHandler)), + featuresCLI.NewStartOutputHandler, + + wire.Bind(new(featuresCLI.StartPresenter), new(presenters.StartPresenter)), + presenters.NewStartPresenter, + + wire.Bind(new(presenters.StartViewer), new(views.StartView)), + views.NewStartView, + + features.NewStartFeature, + ), + ) +} diff --git a/internal/dependencies/aws_stop.go b/internal/dependencies/aws_stop.go new file mode 100644 index 0000000..d94b387 --- /dev/null +++ b/internal/dependencies/aws_stop.go @@ -0,0 +1,63 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + awsProviderUserConfig "github.com/recode-sh/aws-cloud-provider/userconfig" + awsCLI "github.com/recode-sh/cli/internal/aws" + featuresCLI "github.com/recode-sh/cli/internal/features" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/views" + "github.com/recode-sh/recode/features" +) + +func ProvideAWSStopFeature(region, profile, credentialsFilePath, configFilePath string) features.StopFeature { + return provideAWSStopFeature( + awsProviderUserConfig.EnvVarsResolverOpts{ + Region: region, + }, + + awsProviderUserConfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, + + awsCLI.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +func provideAWSStopFeature( + userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, + userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, + userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, +) features.StopFeature { + panic( + wire.Build( + viewSet, + awsServiceBuilderSet, + awsViewableErrorBuilder, + + sshKnownHostsManagerSet, + + stepperSet, + + wire.Bind(new(features.StopOutputHandler), new(featuresCLI.StopOutputHandler)), + featuresCLI.NewStopOutputHandler, + + wire.Bind(new(featuresCLI.StopPresenter), new(presenters.StopPresenter)), + presenters.NewStopPresenter, + + wire.Bind(new(presenters.StopViewer), new(views.StopView)), + views.NewStopView, + + features.NewStopFeature, + ), + ) +} diff --git a/internal/dependencies/aws_uninstall.go b/internal/dependencies/aws_uninstall.go new file mode 100644 index 0000000..ec845fa --- /dev/null +++ b/internal/dependencies/aws_uninstall.go @@ -0,0 +1,61 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + awsProviderUserConfig "github.com/recode-sh/aws-cloud-provider/userconfig" + awsCLI "github.com/recode-sh/cli/internal/aws" + featuresCLI "github.com/recode-sh/cli/internal/features" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/views" + "github.com/recode-sh/recode/features" +) + +func ProvideAWSUninstallFeature(region, profile, credentialsFilePath, configFilePath string) features.UninstallFeature { + return provideAWSUninstallFeature( + awsProviderUserConfig.EnvVarsResolverOpts{ + Region: region, + }, + + awsProviderUserConfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, + + awsCLI.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +func provideAWSUninstallFeature( + userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, + userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, + userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, +) features.UninstallFeature { + panic( + wire.Build( + viewSet, + awsServiceBuilderSet, + awsViewableErrorBuilder, + + stepperSet, + + wire.Bind(new(features.UninstallOutputHandler), new(featuresCLI.UninstallOutputHandler)), + featuresCLI.NewUninstallOutputHandler, + + wire.Bind(new(featuresCLI.UninstallPresenter), new(presenters.UninstallPresenter)), + presenters.NewUninstallPresenter, + + wire.Bind(new(presenters.UninstallViewer), new(views.UninstallView)), + views.NewUninstallView, + + features.NewUninstallFeature, + ), + ) +} diff --git a/internal/dependencies/entities.go b/internal/dependencies/entities.go new file mode 100644 index 0000000..bb0a141 --- /dev/null +++ b/internal/dependencies/entities.go @@ -0,0 +1,38 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + "github.com/recode-sh/cli/internal/entities" +) + +func ProvideDevEnvUserConfigResolver() entities.DevEnvUserConfigResolver { + panic( + wire.Build( + loggerSet, + + userConfigManagerSet, + + githubManagerSet, + + entities.NewDevEnvUserConfigResolver, + ), + ) +} + +func ProvideDevEnvRepositoryResolver() entities.DevEnvRepositoryResolver { + panic( + wire.Build( + loggerSet, + + userConfigManagerSet, + + githubManagerSet, + + entities.NewDevEnvRepositoryResolver, + ), + ) +} diff --git a/internal/dependencies/hooks.go b/internal/dependencies/hooks.go new file mode 100644 index 0000000..ed8e99f --- /dev/null +++ b/internal/dependencies/hooks.go @@ -0,0 +1,39 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + "github.com/recode-sh/cli/internal/hooks" +) + +func ProvidePreRemoveHook() hooks.PreRemove { + panic( + wire.Build( + sshConfigManagerSet, + + sshKnownHostsManagerSet, + + sshKeysManagerSet, + + userConfigManagerSet, + + githubManagerSet, + + hooks.NewPreRemove, + ), + ) +} + +func ProvidePreStopHook() hooks.PreStop { + panic( + wire.Build( + + sshKnownHostsManagerSet, + + hooks.NewPreStop, + ), + ) +} diff --git a/internal/dependencies/login.go b/internal/dependencies/login.go new file mode 100644 index 0000000..820c4b6 --- /dev/null +++ b/internal/dependencies/login.go @@ -0,0 +1,39 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + "github.com/recode-sh/cli/internal/features" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/views" +) + +func ProvideLoginFeature() features.LoginFeature { + panic( + wire.Build( + viewSet, + recodeViewableErrorBuilder, + + loggerSet, + + browserManagerSet, + + userConfigManagerSet, + + sleeperSet, + + githubManagerSet, + + wire.Bind(new(features.LoginPresenter), new(presenters.LoginPresenter)), + presenters.NewLoginPresenter, + + wire.Bind(new(presenters.LoginViewer), new(views.LoginView)), + views.NewLoginView, + + features.NewLoginFeature, + ), + ) +} diff --git a/internal/dependencies/shared.go b/internal/dependencies/shared.go new file mode 100644 index 0000000..6fac601 --- /dev/null +++ b/internal/dependencies/shared.go @@ -0,0 +1,101 @@ +// go:build wireinject +//go:build wireinject +// +build wireinject + +package dependencies + +import ( + "github.com/google/wire" + "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/ssh" + stepperCLI "github.com/recode-sh/cli/internal/stepper" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/cli/internal/views" + "github.com/recode-sh/cli/internal/vscode" + "github.com/recode-sh/recode/github" + "github.com/recode-sh/recode/stepper" +) + +var viewSet = wire.NewSet( + wire.Bind(new(views.Displayer), new(system.Displayer)), + system.NewDisplayer, + views.NewBaseView, +) + +func ProvideBaseView() views.BaseView { + panic( + wire.Build( + viewSet, + ), + ) +} + +var recodeViewableErrorBuilder = wire.NewSet( + wire.Bind(new(presenters.ViewableErrorBuilder), new(presenters.RecodeViewableErrorBuilder)), + presenters.NewRecodeViewableErrorBuilder, +) + +func ProvideRecodeViewableErrorBuilder() presenters.RecodeViewableErrorBuilder { + panic( + wire.Build( + recodeViewableErrorBuilder, + ), + ) +} + +var githubManagerSet = wire.NewSet( + wire.Bind(new(interfaces.GitHubManager), new(github.Service)), + github.NewService, +) + +var userConfigManagerSet = wire.NewSet( + wire.Bind(new(interfaces.UserConfigManager), new(config.UserConfig)), + config.NewUserConfig, +) + +var loggerSet = wire.NewSet( + wire.Bind(new(interfaces.Logger), new(system.Logger)), + system.NewLogger, +) + +var sshConfigManagerSet = wire.NewSet( + wire.Bind(new(interfaces.SSHConfigManager), new(ssh.Config)), + ssh.NewConfigWithDefaultConfigFilePath, +) + +var sshKnownHostsManagerSet = wire.NewSet( + wire.Bind(new(interfaces.SSHKnownHostsManager), new(ssh.KnownHosts)), + ssh.NewKnownHostsWithDefaultKnownHostsFilePath, +) + +var sshKeysManagerSet = wire.NewSet( + wire.Bind(new(interfaces.SSHKeysManager), new(ssh.Keys)), + ssh.NewKeysWithDefaultDir, +) + +var vscodeProcessManagerSet = wire.NewSet( + wire.Bind(new(interfaces.VSCodeProcessManager), new(vscode.Process)), + vscode.NewProcess, +) + +var vscodeExtensionsManagerSet = wire.NewSet( + wire.Bind(new(interfaces.VSCodeExtensionsManager), new(vscode.Extensions)), + vscode.NewExtensions, +) + +var browserManagerSet = wire.NewSet( + wire.Bind(new(interfaces.BrowserManager), new(system.Browser)), + system.NewBrowser, +) + +var sleeperSet = wire.NewSet( + wire.Bind(new(interfaces.Sleeper), new(system.Sleeper)), + system.NewSleeper, +) + +var stepperSet = wire.NewSet( + wire.Bind(new(stepper.Stepper), new(stepperCLI.Stepper)), + stepperCLI.NewStepper, +) diff --git a/internal/dependencies/wire_gen.go b/internal/dependencies/wire_gen.go new file mode 100644 index 0000000..7bc20a1 --- /dev/null +++ b/internal/dependencies/wire_gen.go @@ -0,0 +1,286 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package dependencies + +import ( + "github.com/google/wire" + "github.com/recode-sh/aws-cloud-provider/config" + "github.com/recode-sh/aws-cloud-provider/service" + "github.com/recode-sh/aws-cloud-provider/userconfig" + "github.com/recode-sh/cli/internal/agent" + "github.com/recode-sh/cli/internal/aws" + config2 "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/cli/internal/entities" + features2 "github.com/recode-sh/cli/internal/features" + "github.com/recode-sh/cli/internal/hooks" + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/cli/internal/presenters" + "github.com/recode-sh/cli/internal/ssh" + "github.com/recode-sh/cli/internal/stepper" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/cli/internal/views" + "github.com/recode-sh/cli/internal/vscode" + "github.com/recode-sh/recode/features" + "github.com/recode-sh/recode/github" + stepper2 "github.com/recode-sh/recode/stepper" +) + +// Injectors from aws_remove.go: + +func provideAWSRemoveFeature(userConfigEnvVarsResolverOpts userconfig.EnvVarsResolverOpts, userConfigFilesResolverOpts userconfig.FilesResolverOpts, userConfigLocalResolverOpts aws.UserConfigLocalResolverOpts) features.RemoveFeature { + stepperStepper := stepper.NewStepper() + awsAWSViewableErrorBuilder := aws.NewAWSViewableErrorBuilder() + displayer := system.NewDisplayer() + baseView := views.NewBaseView(displayer) + removeView := views.NewRemoveView(baseView) + removePresenter := presenters.NewRemovePresenter(awsAWSViewableErrorBuilder, removeView) + removeOutputHandler := features2.NewRemoveOutputHandler(removePresenter) + envVars := system.NewEnvVars() + envVarsResolver := userconfig.NewEnvVarsResolver(envVars, userConfigEnvVarsResolverOpts) + profileLoader := config.NewProfileLoader() + filesResolver := userconfig.NewFilesResolver(profileLoader, userConfigFilesResolverOpts) + userConfigLocalResolver := aws.NewUserConfigLocalResolver(envVarsResolver, filesResolver, userConfigLocalResolverOpts) + userConfigValidator := config.NewUserConfigValidator() + userConfigLoader := config.NewUserConfigLoader() + builder := service.NewBuilder(userConfigLocalResolver, userConfigValidator, userConfigLoader) + removeFeature := features.NewRemoveFeature(stepperStepper, removeOutputHandler, builder) + return removeFeature +} + +// Injectors from aws_start.go: + +func provideAWSStartFeature(userConfigEnvVarsResolverOpts userconfig.EnvVarsResolverOpts, userConfigFilesResolverOpts userconfig.FilesResolverOpts, userConfigLocalResolverOpts aws.UserConfigLocalResolverOpts) features.StartFeature { + stepperStepper := stepper.NewStepper() + userConfig := config2.NewUserConfig() + awsAWSViewableErrorBuilder := aws.NewAWSViewableErrorBuilder() + displayer := system.NewDisplayer() + baseView := views.NewBaseView(displayer) + startView := views.NewStartView(baseView) + startPresenter := presenters.NewStartPresenter(awsAWSViewableErrorBuilder, startView) + defaultClientBuilder := agent.NewDefaultClientBuilder() + githubService := github.NewService() + logger := system.NewLogger() + sshConfig := ssh.NewConfigWithDefaultConfigFilePath() + keys := ssh.NewKeysWithDefaultDir() + knownHosts := ssh.NewKnownHostsWithDefaultKnownHostsFilePath() + process := vscode.NewProcess() + extensions := vscode.NewExtensions() + startOutputHandler := features2.NewStartOutputHandler(userConfig, startPresenter, defaultClientBuilder, githubService, logger, sshConfig, keys, knownHosts, process, extensions) + envVars := system.NewEnvVars() + envVarsResolver := userconfig.NewEnvVarsResolver(envVars, userConfigEnvVarsResolverOpts) + profileLoader := config.NewProfileLoader() + filesResolver := userconfig.NewFilesResolver(profileLoader, userConfigFilesResolverOpts) + userConfigLocalResolver := aws.NewUserConfigLocalResolver(envVarsResolver, filesResolver, userConfigLocalResolverOpts) + userConfigValidator := config.NewUserConfigValidator() + userConfigLoader := config.NewUserConfigLoader() + builder := service.NewBuilder(userConfigLocalResolver, userConfigValidator, userConfigLoader) + startFeature := features.NewStartFeature(stepperStepper, startOutputHandler, builder) + return startFeature +} + +// Injectors from aws_stop.go: + +func provideAWSStopFeature(userConfigEnvVarsResolverOpts userconfig.EnvVarsResolverOpts, userConfigFilesResolverOpts userconfig.FilesResolverOpts, userConfigLocalResolverOpts aws.UserConfigLocalResolverOpts) features.StopFeature { + stepperStepper := stepper.NewStepper() + awsAWSViewableErrorBuilder := aws.NewAWSViewableErrorBuilder() + displayer := system.NewDisplayer() + baseView := views.NewBaseView(displayer) + stopView := views.NewStopView(baseView) + stopPresenter := presenters.NewStopPresenter(awsAWSViewableErrorBuilder, stopView) + knownHosts := ssh.NewKnownHostsWithDefaultKnownHostsFilePath() + stopOutputHandler := features2.NewStopOutputHandler(stopPresenter, knownHosts) + envVars := system.NewEnvVars() + envVarsResolver := userconfig.NewEnvVarsResolver(envVars, userConfigEnvVarsResolverOpts) + profileLoader := config.NewProfileLoader() + filesResolver := userconfig.NewFilesResolver(profileLoader, userConfigFilesResolverOpts) + userConfigLocalResolver := aws.NewUserConfigLocalResolver(envVarsResolver, filesResolver, userConfigLocalResolverOpts) + userConfigValidator := config.NewUserConfigValidator() + userConfigLoader := config.NewUserConfigLoader() + builder := service.NewBuilder(userConfigLocalResolver, userConfigValidator, userConfigLoader) + stopFeature := features.NewStopFeature(stepperStepper, stopOutputHandler, builder) + return stopFeature +} + +// Injectors from aws_uninstall.go: + +func provideAWSUninstallFeature(userConfigEnvVarsResolverOpts userconfig.EnvVarsResolverOpts, userConfigFilesResolverOpts userconfig.FilesResolverOpts, userConfigLocalResolverOpts aws.UserConfigLocalResolverOpts) features.UninstallFeature { + stepperStepper := stepper.NewStepper() + awsAWSViewableErrorBuilder := aws.NewAWSViewableErrorBuilder() + displayer := system.NewDisplayer() + baseView := views.NewBaseView(displayer) + uninstallView := views.NewUninstallView(baseView) + uninstallPresenter := presenters.NewUninstallPresenter(awsAWSViewableErrorBuilder, uninstallView) + uninstallOutputHandler := features2.NewUninstallOutputHandler(uninstallPresenter) + envVars := system.NewEnvVars() + envVarsResolver := userconfig.NewEnvVarsResolver(envVars, userConfigEnvVarsResolverOpts) + profileLoader := config.NewProfileLoader() + filesResolver := userconfig.NewFilesResolver(profileLoader, userConfigFilesResolverOpts) + userConfigLocalResolver := aws.NewUserConfigLocalResolver(envVarsResolver, filesResolver, userConfigLocalResolverOpts) + userConfigValidator := config.NewUserConfigValidator() + userConfigLoader := config.NewUserConfigLoader() + builder := service.NewBuilder(userConfigLocalResolver, userConfigValidator, userConfigLoader) + uninstallFeature := features.NewUninstallFeature(stepperStepper, uninstallOutputHandler, builder) + return uninstallFeature +} + +// Injectors from entities.go: + +func ProvideDevEnvUserConfigResolver() entities.DevEnvUserConfigResolver { + logger := system.NewLogger() + userConfig := config2.NewUserConfig() + githubService := github.NewService() + devEnvUserConfigResolver := entities.NewDevEnvUserConfigResolver(logger, userConfig, githubService) + return devEnvUserConfigResolver +} + +func ProvideDevEnvRepositoryResolver() entities.DevEnvRepositoryResolver { + logger := system.NewLogger() + userConfig := config2.NewUserConfig() + githubService := github.NewService() + devEnvRepositoryResolver := entities.NewDevEnvRepositoryResolver(logger, userConfig, githubService) + return devEnvRepositoryResolver +} + +// Injectors from hooks.go: + +func ProvidePreRemoveHook() hooks.PreRemove { + sshConfig := ssh.NewConfigWithDefaultConfigFilePath() + keys := ssh.NewKeysWithDefaultDir() + knownHosts := ssh.NewKnownHostsWithDefaultKnownHostsFilePath() + userConfig := config2.NewUserConfig() + githubService := github.NewService() + preRemove := hooks.NewPreRemove(sshConfig, keys, knownHosts, userConfig, githubService) + return preRemove +} + +func ProvidePreStopHook() hooks.PreStop { + knownHosts := ssh.NewKnownHostsWithDefaultKnownHostsFilePath() + preStop := hooks.NewPreStop(knownHosts) + return preStop +} + +// Injectors from login.go: + +func ProvideLoginFeature() features2.LoginFeature { + presentersRecodeViewableErrorBuilder := presenters.NewRecodeViewableErrorBuilder() + displayer := system.NewDisplayer() + baseView := views.NewBaseView(displayer) + loginView := views.NewLoginView(baseView) + loginPresenter := presenters.NewLoginPresenter(presentersRecodeViewableErrorBuilder, loginView) + logger := system.NewLogger() + browser := system.NewBrowser() + userConfig := config2.NewUserConfig() + sleeper := system.NewSleeper() + githubService := github.NewService() + loginFeature := features2.NewLoginFeature(loginPresenter, logger, browser, userConfig, sleeper, githubService) + return loginFeature +} + +// Injectors from shared.go: + +func ProvideBaseView() views.BaseView { + displayer := system.NewDisplayer() + baseView := views.NewBaseView(displayer) + return baseView +} + +func ProvideRecodeViewableErrorBuilder() presenters.RecodeViewableErrorBuilder { + presentersRecodeViewableErrorBuilder := presenters.NewRecodeViewableErrorBuilder() + return presentersRecodeViewableErrorBuilder +} + +// aws_remove.go: + +func ProvideAWSRemoveFeature(region, profile, credentialsFilePath, configFilePath string) features.RemoveFeature { + return provideAWSRemoveFeature(userconfig.EnvVarsResolverOpts{ + Region: region, + }, userconfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, aws.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +// aws_start.go: + +func ProvideAWSStartFeature(region, profile, credentialsFilePath, configFilePath string) features.StartFeature { + return provideAWSStartFeature(userconfig.EnvVarsResolverOpts{ + Region: region, + }, userconfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, aws.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +// aws_stop.go: + +func ProvideAWSStopFeature(region, profile, credentialsFilePath, configFilePath string) features.StopFeature { + return provideAWSStopFeature(userconfig.EnvVarsResolverOpts{ + Region: region, + }, userconfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, aws.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +// aws_uninstall.go: + +func ProvideAWSUninstallFeature(region, profile, credentialsFilePath, configFilePath string) features.UninstallFeature { + return provideAWSUninstallFeature(userconfig.EnvVarsResolverOpts{ + Region: region, + }, userconfig.FilesResolverOpts{ + Region: region, + Profile: profile, + CredentialsFilePath: credentialsFilePath, + ConfigFilePath: configFilePath, + }, aws.UserConfigLocalResolverOpts{ + Profile: profile, + }, + ) +} + +// shared.go: + +var viewSet = wire.NewSet(wire.Bind(new(views.Displayer), new(system.Displayer)), system.NewDisplayer, views.NewBaseView) + +var recodeViewableErrorBuilder = wire.NewSet(wire.Bind(new(presenters.ViewableErrorBuilder), new(presenters.RecodeViewableErrorBuilder)), presenters.NewRecodeViewableErrorBuilder) + +var githubManagerSet = wire.NewSet(wire.Bind(new(interfaces.GitHubManager), new(github.Service)), github.NewService) + +var userConfigManagerSet = wire.NewSet(wire.Bind(new(interfaces.UserConfigManager), new(config2.UserConfig)), config2.NewUserConfig) + +var loggerSet = wire.NewSet(wire.Bind(new(interfaces.Logger), new(system.Logger)), system.NewLogger) + +var sshConfigManagerSet = wire.NewSet(wire.Bind(new(interfaces.SSHConfigManager), new(ssh.Config)), ssh.NewConfigWithDefaultConfigFilePath) + +var sshKnownHostsManagerSet = wire.NewSet(wire.Bind(new(interfaces.SSHKnownHostsManager), new(ssh.KnownHosts)), ssh.NewKnownHostsWithDefaultKnownHostsFilePath) + +var sshKeysManagerSet = wire.NewSet(wire.Bind(new(interfaces.SSHKeysManager), new(ssh.Keys)), ssh.NewKeysWithDefaultDir) + +var vscodeProcessManagerSet = wire.NewSet(wire.Bind(new(interfaces.VSCodeProcessManager), new(vscode.Process)), vscode.NewProcess) + +var vscodeExtensionsManagerSet = wire.NewSet(wire.Bind(new(interfaces.VSCodeExtensionsManager), new(vscode.Extensions)), vscode.NewExtensions) + +var browserManagerSet = wire.NewSet(wire.Bind(new(interfaces.BrowserManager), new(system.Browser)), system.NewBrowser) + +var sleeperSet = wire.NewSet(wire.Bind(new(interfaces.Sleeper), new(system.Sleeper)), system.NewSleeper) + +var stepperSet = wire.NewSet(wire.Bind(new(stepper2.Stepper), new(stepper.Stepper)), stepper.NewStepper) diff --git a/internal/entities/dev_env.go b/internal/entities/dev_env.go new file mode 100644 index 0000000..cbd11d1 --- /dev/null +++ b/internal/entities/dev_env.go @@ -0,0 +1,6 @@ +package entities + +type DevEnvAdditionalProperties struct { + GitHubCreatedSSHKeyId *int64 `json:"github_created_ssh_key_id"` + GitHubCreatedGPGKeyId *int64 `json:"github_created_gpg_key_id"` +} diff --git a/internal/entities/dev_env_repository_resolver.go b/internal/entities/dev_env_repository_resolver.go new file mode 100644 index 0000000..926b741 --- /dev/null +++ b/internal/entities/dev_env_repository_resolver.go @@ -0,0 +1,124 @@ +package entities + +import ( + // "github.com/fatih/color" + // "github.com/google/go-github/v43/github" + "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/recode/entities" + "github.com/recode-sh/recode/github" +) + +type DevEnvRepositoryResolver struct { + logger interfaces.Logger + userConfig interfaces.UserConfigManager + github interfaces.GitHubManager +} + +func NewDevEnvRepositoryResolver( + logger interfaces.Logger, + userConfig interfaces.UserConfigManager, + github interfaces.GitHubManager, +) DevEnvRepositoryResolver { + + return DevEnvRepositoryResolver{ + logger: logger, + userConfig: userConfig, + github: github, + } +} + +func (d DevEnvRepositoryResolver) Resolve( + repositoryName string, + checkForRepositoryExistence bool, +) (*entities.ResolvedDevEnvRepository, error) { + + githubAccessToken := d.userConfig.GetString( + config.UserConfigKeyGitHubAccessToken, + ) + + githubUsername := d.userConfig.GetString( + config.UserConfigKeyGitHubUsername, + ) + + parsedRepoName, err := github.ParseRepositoryName( + repositoryName, + githubUsername, + ) + + if err != nil { + // If repository name is invalid, we are sure + // that the repository doesn't exist. + return nil, entities.ErrDevEnvRepositoryNotFound{ + RepoOwner: githubUsername, + RepoName: repositoryName, + } + } + + if checkForRepositoryExistence { + repoExists, err := d.github.DoesRepositoryExist( + githubAccessToken, + parsedRepoName.Owner, + parsedRepoName.Name, + ) + + if err != nil { + return nil, err + } + + // if !repoExists && parsedRepoName.Owner != githubUsername { + if !repoExists { + return nil, entities.ErrDevEnvRepositoryNotFound{ + RepoOwner: parsedRepoName.Owner, + RepoName: parsedRepoName.Name, + } + } + } + + // if !repoExists { + // bold := color.New(color.Bold).SprintFunc() + + // d.logger.Log( + // "\n%s "+bold("Repository \"%s\" not found. Creating now..."), + // bold(color.YellowString("Warning!")), + // parsedRepoName.Name, + // ) + + // // Means that we want the repository to be created + // // in the logged user personal account. See GitHub SDK docs. + // createdRepoOrganization := "" + + // createdRepoIsPrivate := true + // createdRepoProps := &github.Repository{ + // Name: &parsedRepoName.Name, + // Private: &createdRepoIsPrivate, + // } + + // _, err := d.github.CreateRepository( + // githubAccessToken, + // createdRepoOrganization, + // createdRepoProps, + // ) + + // if err != nil { + // return nil, err + // } + // } + + return &entities.ResolvedDevEnvRepository{ + Owner: parsedRepoName.Owner, + ExplicitOwner: parsedRepoName.ExplicitOwner, + + Name: parsedRepoName.Name, + + GitURL: github.BuildGitURL( + parsedRepoName.Owner, + parsedRepoName.Name, + ), + + GitHTTPURL: github.BuildGitHTTPURL( + parsedRepoName.Owner, + parsedRepoName.Name, + ), + }, nil +} diff --git a/internal/entities/dev_env_user_config_resolver.go b/internal/entities/dev_env_user_config_resolver.go new file mode 100644 index 0000000..2acda92 --- /dev/null +++ b/internal/entities/dev_env_user_config_resolver.go @@ -0,0 +1,110 @@ +package entities + +import ( + "fmt" + + "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/recode/entities" + "github.com/recode-sh/recode/github" +) + +type DevEnvUserConfigResolver struct { + logger interfaces.Logger + userConfig interfaces.UserConfigManager + github interfaces.GitHubManager +} + +func NewDevEnvUserConfigResolver( + logger interfaces.Logger, + userConfig interfaces.UserConfigManager, + github interfaces.GitHubManager, +) DevEnvUserConfigResolver { + + return DevEnvUserConfigResolver{ + logger: logger, + userConfig: userConfig, + github: github, + } +} + +func (d DevEnvUserConfigResolver) Resolve() ( + *entities.ResolvedDevEnvUserConfig, + error, +) { + + githubAccessToken := d.userConfig.GetString( + config.UserConfigKeyGitHubAccessToken, + ) + + devEnvUserConfigRepoOwner := d.userConfig.GetString( + config.UserConfigKeyGitHubUsername, + ) + + userHasDevEnvUserConfigRepo, err := d.github.DoesRepositoryExist( + githubAccessToken, + devEnvUserConfigRepoOwner, + entities.DevEnvUserConfigRepoName, + ) + + if err != nil { + return nil, err + } + + if !userHasDevEnvUserConfigRepo { + bold := constants.Bold + yellow := constants.Yellow + + d.logger.Log( + "\n%s "+bold("No repository \"%s\" found in GitHub account \"%s\"."), + bold(yellow("Warning!")), + entities.DevEnvUserConfigRepoName, + devEnvUserConfigRepoOwner, + ) + + d.logger.Log( + "\n%s "+bold("Fallback to \"%s\" for user base configuration."), + bold(yellow("Warning!")), + entities.DevEnvUserConfigDefaultRepoOwner+"/"+entities.DevEnvUserConfigRepoName, + ) + + devEnvUserConfigRepoOwner = entities.DevEnvUserConfigDefaultRepoOwner + } + + _, err = d.github.GetFileContentFromRepository( + githubAccessToken, + devEnvUserConfigRepoOwner, + entities.DevEnvUserConfigRepoName, + entities.DevEnvUserConfigDockerfileFileName, + ) + + if err != nil && d.github.IsNotFoundError(err) { + return nil, entities.ErrInvalidDevEnvUserConfig{ + RepoOwner: devEnvUserConfigRepoOwner, + Reason: fmt.Sprintf( + "Your repository must contain a file named \"%s\".", + entities.DevEnvUserConfigDockerfileFileName, + ), + } + } + + if err != nil { + return nil, err + } + + return &entities.ResolvedDevEnvUserConfig{ + RepoOwner: devEnvUserConfigRepoOwner, + RepoName: entities.DevEnvUserConfigRepoName, + + RepoGitURL: github.BuildGitURL( + devEnvUserConfigRepoOwner, + entities.DevEnvUserConfigRepoName, + ), + + RepoGitHTTPURL: github.BuildGitHTTPURL( + devEnvUserConfigRepoOwner, + entities.DevEnvUserConfigRepoName, + ), + }, nil +} diff --git a/internal/exceptions/requirements.go b/internal/exceptions/requirements.go new file mode 100644 index 0000000..42d3caa --- /dev/null +++ b/internal/exceptions/requirements.go @@ -0,0 +1,9 @@ +package exceptions + +type ErrMissingRequirements struct { + MissingRequirements []string +} + +func (ErrMissingRequirements) Error() string { + return "ErrMissingRequirements" +} diff --git a/internal/exceptions/user.go b/internal/exceptions/user.go new file mode 100644 index 0000000..7b31169 --- /dev/null +++ b/internal/exceptions/user.go @@ -0,0 +1,15 @@ +package exceptions + +import "errors" + +var ( + ErrUserNotLoggedIn = errors.New("ErrUserNotLoggedIn") +) + +type ErrLoginError struct { + Reason string +} + +func (ErrLoginError) Error() string { + return "ErrLoginError" +} diff --git a/internal/features/login.go b/internal/features/login.go new file mode 100644 index 0000000..b5eba23 --- /dev/null +++ b/internal/features/login.go @@ -0,0 +1,216 @@ +package features + +import ( + "errors" + "fmt" + "html" + "net" + "net/http" + "net/url" + "time" + + "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/exceptions" + "github.com/recode-sh/cli/internal/interfaces" + "golang.org/x/oauth2" +) + +type LoginInput struct{} + +type LoginResponseContent struct{} + +type LoginResponse struct { + Error error + Content LoginResponseContent +} + +type LoginPresenter interface { + PresentToView(LoginResponse) +} + +type LoginFeature struct { + presenter LoginPresenter + logger interfaces.Logger + browser interfaces.BrowserManager + userConfig interfaces.UserConfigManager + sleeper interfaces.Sleeper + github interfaces.GitHubManager +} + +func NewLoginFeature( + presenter LoginPresenter, + logger interfaces.Logger, + browser interfaces.BrowserManager, + config interfaces.UserConfigManager, + sleeper interfaces.Sleeper, + github interfaces.GitHubManager, +) LoginFeature { + + return LoginFeature{ + presenter: presenter, + logger: logger, + browser: browser, + userConfig: config, + sleeper: sleeper, + github: github, + } +} + +func (l LoginFeature) Execute(input LoginInput) error { + handleError := func(err error) error { + l.presenter.PresentToView(LoginResponse{ + Error: exceptions.ErrLoginError{ + Reason: err.Error(), + }, + }) + + return err + } + + gitHubOAuthCbHandlerResp := struct { + Error error + AccessToken string + }{} + + gitHubOauthCbHandlerDoneChan := make(chan struct{}) + + gitHubOAuthCbHandler := func(w http.ResponseWriter, r *http.Request) { + defer close(gitHubOauthCbHandlerDoneChan) + + queryComponents, err := url.ParseQuery(r.URL.RawQuery) + + if err != nil { + gitHubOAuthCbHandlerResp.Error = err + return + } + + errorInQuery, hasErrorInQuery := queryComponents["error"] + + if hasErrorInQuery { + msg := "

Error!

" + msg = msg + "

" + html.EscapeString(errorInQuery[0]) + "

" + + w.WriteHeader(500) + w.Write([]byte(msg)) + + gitHubOAuthCbHandlerResp.Error = errors.New(errorInQuery[0]) + return + } + + accessTokenInQuery, hasAccessTokenInQuery := queryComponents["access_token"] + + if !hasAccessTokenInQuery { + msg := "

Error!

" + msg = msg + "

An unknown error occured during GitHub connection. Please retry.

" + + w.WriteHeader(500) + w.Write([]byte(msg)) + + gitHubOAuthCbHandlerResp.Error = errors.New("no access token returned after authorization") + return + } + + msg := "

Success!

" + msg = msg + "

Your GitHub account is now connected. You can close this tab and go back to the Recode CLI.

" + + w.WriteHeader(200) + w.Write([]byte(msg)) + + gitHubOAuthCbHandlerResp.AccessToken = accessTokenInQuery[0] + } // <- End of gitHubOAuthCbHandler + + http.HandleFunc( + config.GitHubOAuthAPIToCLIURLPath, + gitHubOAuthCbHandler, + ) + + // Assign a random port to our http server + httpListener, err := net.Listen("tcp", ":0") + + if err != nil { + return handleError(err) + } + + httpServerServeErrorChan := make(chan error, 1) + go func() { + httpServerServeErrorChan <- http.Serve(httpListener, nil) + }() + + httpListenPort := httpListener.Addr().(*net.TCPAddr).Port + + gitHubOAuthClient := &oauth2.Config{ + ClientID: config.GitHubOAuthClientID, + Scopes: config.GitHubOAuthScopes, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://github.com/login/oauth/authorize", + }, + RedirectURL: config.GitHubOAuthCLIToAPIURL, + } + + // Listen port is passed through OAuth + // state because GitHub doesn't support + // dynamic redirect URIs + gitHubOAuthAuthorizeURL := gitHubOAuthClient.AuthCodeURL( + fmt.Sprintf("%d", httpListenPort), + ) + + bold := constants.Bold + l.logger.Log(bold("\nYou will be taken to your browser to connect your GitHub account...\n")) + + l.logger.Info("If your browser doesn't open automatically, go to the following link:\n") + l.logger.Log("%s", gitHubOAuthAuthorizeURL) + + l.sleeper.Sleep(4 * time.Second) + + if err := l.browser.OpenURL(gitHubOAuthAuthorizeURL); err != nil { + l.logger.Error( + "\nCannot open browser! Please visit above URL.", + ) + } + + l.logger.Warning("\nWaiting for GitHub authorization... (Press Ctrl-C to quit)\n") + + select { + case httpServerServeError := <-httpServerServeErrorChan: + return handleError(httpServerServeError) + case <-gitHubOauthCbHandlerDoneChan: + // We swallow the httpListener.Close() error here + // given that the CLI will exit and force all + // resources to be released + _ = httpListener.Close() + } + + if gitHubOAuthCbHandlerResp.Error != nil { + return handleError(gitHubOAuthCbHandlerResp.Error) + } + + githubUser, err := l.github.GetAuthenticatedUser( + gitHubOAuthCbHandlerResp.AccessToken, + ) + + if err != nil { + return handleError(err) + } + + l.userConfig.Set( + config.UserConfigKeyUserIsLoggedIn, + true, + ) + + l.userConfig.Set( + config.UserConfigKeyGitHubAccessToken, + gitHubOAuthCbHandlerResp.AccessToken, + ) + + l.userConfig.PopulateFromGitHubUser( + githubUser, + ) + + if err := l.userConfig.WriteConfig(); err != nil { + return handleError(err) + } + + l.presenter.PresentToView(LoginResponse{}) + return nil +} diff --git a/internal/features/remove.go b/internal/features/remove.go new file mode 100644 index 0000000..309252f --- /dev/null +++ b/internal/features/remove.go @@ -0,0 +1,57 @@ +package features + +import ( + "github.com/recode-sh/recode/features" +) + +type RemoveResponse struct { + Error error + Content RemoveResponseContent +} + +type RemoveResponseContent struct { + DevEnvName string +} + +type RemovePresenter interface { + PresentToView(RemoveResponse) +} + +type RemoveOutputHandler struct { + presenter RemovePresenter +} + +func NewRemoveOutputHandler( + presenter RemovePresenter, +) RemoveOutputHandler { + + return RemoveOutputHandler{ + presenter: presenter, + } +} + +func (r RemoveOutputHandler) HandleOutput(output features.RemoveOutput) error { + output.Stepper.StopCurrentStep() + + handleError := func(err error) error { + r.presenter.PresentToView(RemoveResponse{ + Error: err, + }) + + return err + } + + if output.Error != nil { + return handleError(output.Error) + } + + devEnv := output.Content.DevEnv + + r.presenter.PresentToView(RemoveResponse{ + Content: RemoveResponseContent{ + DevEnvName: devEnv.Name, + }, + }) + + return nil +} diff --git a/internal/features/start.go b/internal/features/start.go new file mode 100644 index 0000000..962575b --- /dev/null +++ b/internal/features/start.go @@ -0,0 +1,442 @@ +package features + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "strconv" + "strings" + + "github.com/recode-sh/agent/constants" + "github.com/recode-sh/agent/proto" + "github.com/recode-sh/cli/internal/agent" + "github.com/recode-sh/cli/internal/config" + cliConstants "github.com/recode-sh/cli/internal/constants" + cliEntities "github.com/recode-sh/cli/internal/entities" + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/recode/actions" + "github.com/recode-sh/recode/entities" + "github.com/recode-sh/recode/features" +) + +type StartResponse struct { + Error error + Content StartResponseContent +} + +type StartResponseContent struct { + DevEnvName string + DevEnvAlreadyStarted bool + DevEnvRebuilt bool +} + +type StartPresenter interface { + PresentToView(StartResponse) +} + +type StartOutputHandler struct { + userConfig interfaces.UserConfigManager + presenter StartPresenter + agentClientBuilder agent.ClientBuilder + github interfaces.GitHubManager + logger interfaces.Logger + sshConfig interfaces.SSHConfigManager + sshKeys interfaces.SSHKeysManager + sshKnownHosts interfaces.SSHKnownHostsManager + vscodeProcess interfaces.VSCodeProcessManager + vscodeExtensions interfaces.VSCodeExtensionsManager +} + +func NewStartOutputHandler( + userConfig interfaces.UserConfigManager, + presenter StartPresenter, + agentClientBuilder agent.ClientBuilder, + github interfaces.GitHubManager, + logger interfaces.Logger, + sshConfig interfaces.SSHConfigManager, + sshKeys interfaces.SSHKeysManager, + sshKnownHosts interfaces.SSHKnownHostsManager, + vscodeProcess interfaces.VSCodeProcessManager, + vscodeExtensions interfaces.VSCodeExtensionsManager, +) StartOutputHandler { + + return StartOutputHandler{ + userConfig: userConfig, + presenter: presenter, + agentClientBuilder: agentClientBuilder, + github: github, + logger: logger, + sshConfig: sshConfig, + sshKnownHosts: sshKnownHosts, + sshKeys: sshKeys, + vscodeProcess: vscodeProcess, + vscodeExtensions: vscodeExtensions, + } +} + +func (s StartOutputHandler) HandleOutput(output features.StartOutput) error { + + stepper := output.Stepper + + handleError := func(err error) error { + stepper.StopCurrentStep() + + s.presenter.PresentToView(StartResponse{ + Error: err, + }) + + return err + } + + if output.Error != nil { + return handleError(output.Error) + } + + devEnv := output.Content.DevEnv + + devEnvCreated := output.Content.DevEnvCreated + devEnvStarted := output.Content.DevEnvStarted + devEnvRebuildAsked := output.Content.DevEnvRebuildAsked + + devEnvAlreadyStarted := !devEnvCreated && !devEnvStarted && !devEnvRebuildAsked + + var devEnvAdditionalProperties *cliEntities.DevEnvAdditionalProperties + + if len(devEnv.AdditionalPropertiesJSON) > 0 { + err := json.Unmarshal( + []byte(devEnv.AdditionalPropertiesJSON), + &devEnvAdditionalProperties, + ) + + if err != nil { + return handleError(err) + } + } + + if devEnvAdditionalProperties == nil { + devEnvAdditionalProperties = &cliEntities.DevEnvAdditionalProperties{} + } + + if devEnvCreated || devEnvRebuildAsked { + if !devEnvRebuildAsked { + stepper.StartTemporaryStep( + "Building your development environment", + ) + } + + agentClient := s.agentClientBuilder.Build( + agent.NewDefaultClientConfig( + []byte(devEnv.SSHKeyPairPEMContent), + devEnv.InstancePublicIPAddress, + ), + ) + + err := agentClient.InitInstance(&proto.InitInstanceRequest{ + DevEnvNameSlug: devEnv.GetNameSlug(), + GithubUserEmail: s.userConfig.GetString(config.UserConfigKeyGitHubEmail), + UserFullName: s.userConfig.GetString(config.UserConfigKeyGitHubFullName), + }, func(stream agent.InitInstanceStream) error { + + for { + initInstanceReply, err := stream.Recv() + + if err == io.EOF { + break + } + + if err != nil { + return err + } + + if initInstanceReply.GithubSshPublicKeyContent != nil && + devEnvAdditionalProperties.GitHubCreatedSSHKeyId == nil { + + sshKeyInGitHub, err := s.github.CreateSSHKey( + s.userConfig.GetString(config.UserConfigKeyGitHubAccessToken), + fmt.Sprintf("recode-%s", devEnv.GetNameSlug()), + initInstanceReply.GetGithubSshPublicKeyContent(), + ) + + if err != nil { + return err + } + + devEnvAdditionalProperties.GitHubCreatedSSHKeyId = sshKeyInGitHub.ID + err = devEnv.SetAdditionalPropertiesJSON(devEnvAdditionalProperties) + + if err != nil { + return err + } + + err = actions.UpdateDevEnvInConfig( + stepper, + output.Content.CloudService, + output.Content.RecodeConfig, + output.Content.Cluster, + devEnv, + ) + + if err != nil { + return err + } + } + + if initInstanceReply.GithubGpgPublicKeyContent != nil && + devEnvAdditionalProperties.GitHubCreatedGPGKeyId == nil { + + gpgKeyInGitHub, err := s.github.CreateGPGKey( + s.userConfig.GetString(config.UserConfigKeyGitHubAccessToken), + initInstanceReply.GetGithubGpgPublicKeyContent(), + ) + + if err != nil { + return err + } + + devEnvAdditionalProperties.GitHubCreatedGPGKeyId = gpgKeyInGitHub.ID + err = devEnv.SetAdditionalPropertiesJSON(devEnvAdditionalProperties) + + if err != nil { + return err + } + + err = actions.UpdateDevEnvInConfig( + stepper, + output.Content.CloudService, + output.Content.RecodeConfig, + output.Content.Cluster, + devEnv, + ) + + if err != nil { + return err + } + } + } + + return nil + }) + + if err != nil { + return handleError(err) + } + + resolvedDevEnvUserConfig := devEnv.ResolvedUserConfig + resolvedRepository := devEnv.ResolvedRepository + + err = agentClient.BuildAndStartDevEnv( + &proto.BuildAndStartDevEnvRequest{ + DevEnvRepoOwner: resolvedRepository.Owner, + DevEnvRepoName: resolvedRepository.Name, + UserConfigRepoOwner: resolvedDevEnvUserConfig.RepoOwner, + UserConfigRepoName: resolvedDevEnvUserConfig.RepoName, + }, + func(stream agent.BuildAndStartDevEnvStream) error { + + var hasUncompletedLogLine = false + var uncompletedLogLineBuf bytes.Buffer + + newLineIndentSpaces := " " + formatLogLine := func(logLine string) string { + return strings.TrimPrefix(strings.ReplaceAll( + strings.ReplaceAll( + logLine, "\n", "\n"+newLineIndentSpaces, + ), + "\r", + "\r"+newLineIndentSpaces, + ), "\n"+newLineIndentSpaces) + } + + for { + startDevEnvReply, err := stream.Recv() + + // Make sure to display all logs + // especially in case of error + if uncompletedLogLineBuf.Len() > 0 { + s.logger.LogNoNewline(strings.TrimSuffix(uncompletedLogLineBuf.String(), "\n")) + uncompletedLogLineBuf = bytes.Buffer{} + } + + if err == io.EOF { + break + } + + if err != nil { + return err + } + + stepper.StopCurrentStep() + + if len(startDevEnvReply.LogLineHeader) > 0 { + bold := cliConstants.Bold + blue := cliConstants.Blue + s.logger.Log(bold(blue("> "+startDevEnvReply.LogLineHeader)) + "\n") + } + + if len(startDevEnvReply.LogLine) > 0 { + goLoggerNoNewLine := log.New(&uncompletedLogLineBuf, " ", log.Flags()) + goLoggerNewLine := log.New(s.logger, " ", log.Flags()) + + // No prefix for empty new lines + if startDevEnvReply.LogLine == "\n" { + hasUncompletedLogLine = false + + s.logger.Log("\n") + + continue + } + + if strings.HasSuffix(startDevEnvReply.LogLine, "\n") && + !strings.Contains(startDevEnvReply.LogLine, "\r") { + + if hasUncompletedLogLine { + hasUncompletedLogLine = false + + // Ends the log line + // and add a new line at end + s.logger.Log(strings.TrimSuffix( + formatLogLine( + startDevEnvReply.LogLine, + ), + "\n"+newLineIndentSpaces, + ) + "\n") + + continue + } + + hasUncompletedLogLine = false + + // Start a complete log line by prefix + // and add a new line at end + goLoggerNewLine.Print(strings.TrimSuffix( + formatLogLine( + startDevEnvReply.LogLine, + ), + "\n"+newLineIndentSpaces, + ) + "\n") + + continue + } + + if !hasUncompletedLogLine { + // Start an uncompleted log line by prefix + // but without a new line at end + goLoggerNoNewLine.Print(formatLogLine( + startDevEnvReply.LogLine, + )) + + hasUncompletedLogLine = true + continue + } + + // Continue logging uncompleted log line + // without adding prefix or new line + s.logger.LogNoNewline(formatLogLine( + startDevEnvReply.LogLine, + )) + } + } + + return nil + }, + ) + + if err != nil { + return handleError(err) + } + } + + if !devEnvAlreadyStarted { + err := output.Content.SetDevEnvAsStarted() + + if err != nil { + return handleError(err) + } + } + + stepper.StartTemporaryStepWithoutNewLine( + "Updating your local SSH configuration", + ) + + sshPEMPath, err := s.sshKeys.CreateOrReplacePEM( + devEnv.GetSSHKeyPairName(), + devEnv.SSHKeyPairPEMContent, + ) + + if err != nil { + return handleError(err) + } + + sshServerListenPort, err := strconv.ParseInt( + constants.SSHServerListenPort, + 10, + 64, + ) + + if err != nil { + return handleError(err) + } + + sshConfigHostKey := devEnv.Name + + err = s.sshConfig.AddOrReplaceHost( + sshConfigHostKey, + devEnv.InstancePublicIPAddress, + sshPEMPath, + entities.DevEnvRootUser, + sshServerListenPort, + ) + + if err != nil { + return handleError(err) + } + + for _, sshHostKey := range devEnv.SSHHostKeys { + err := s.sshKnownHosts.AddOrReplace( + devEnv.InstancePublicIPAddress, + sshHostKey.Algorithm, + sshHostKey.Fingerprint, + ) + + if err != nil { + return handleError(err) + } + } + + stepper.StartTemporaryStepWithoutNewLine( + "Installing Visual Studio Code Remote - SSH extension", + ) + + _, err = s.vscodeExtensions.Install("ms-vscode-remote.remote-ssh") + + if err != nil { + return handleError(err) + } + + stepper.StartTemporaryStepWithoutNewLine( + "Opening Visual Studio Code", + ) + + _, err = s.vscodeProcess.OpenOnRemote( + sshConfigHostKey, + constants.DevEnvVSCodeWorkspaceConfigFilePath, + ) + + if err != nil { + return handleError(err) + } + + stepper.StopCurrentStep() + + s.presenter.PresentToView(StartResponse{ + Content: StartResponseContent{ + DevEnvName: devEnv.Name, + DevEnvAlreadyStarted: devEnvAlreadyStarted, + DevEnvRebuilt: devEnvRebuildAsked, + }, + }) + + return nil +} diff --git a/internal/features/stop.go b/internal/features/stop.go new file mode 100644 index 0000000..f288a44 --- /dev/null +++ b/internal/features/stop.go @@ -0,0 +1,72 @@ +package features + +import ( + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/recode/features" +) + +type StopResponse struct { + Error error + Content StopResponseContent +} + +type StopResponseContent struct { + DevEnvName string + DevEnvAlreadyStopped bool +} + +type StopPresenter interface { + PresentToView(StopResponse) +} + +type StopOutputHandler struct { + presenter StopPresenter + sshKnownHosts interfaces.SSHKnownHostsManager +} + +func NewStopOutputHandler( + presenter StopPresenter, + sshKnownHosts interfaces.SSHKnownHostsManager, +) StopOutputHandler { + + return StopOutputHandler{ + presenter: presenter, + sshKnownHosts: sshKnownHosts, + } +} + +func (s StopOutputHandler) HandleOutput(output features.StopOutput) error { + output.Stepper.StopCurrentStep() + + handleError := func(err error) error { + s.presenter.PresentToView(StopResponse{ + Error: err, + }) + + return err + } + + if output.Error != nil { + return handleError(output.Error) + } + + devEnv := output.Content.DevEnv + devEnvAlreadyStopped := output.Content.DevEnvAlreadyStopped + + if !devEnvAlreadyStopped { + err := output.Content.SetDevEnvAsStopped() + + if err != nil { + return handleError(err) + } + } + + s.presenter.PresentToView(StopResponse{ + Content: StopResponseContent{ + DevEnvName: devEnv.Name, + DevEnvAlreadyStopped: devEnvAlreadyStopped, + }, + }) + + return nil +} diff --git a/internal/features/uninstall.go b/internal/features/uninstall.go new file mode 100644 index 0000000..5eaa051 --- /dev/null +++ b/internal/features/uninstall.go @@ -0,0 +1,74 @@ +package features + +import ( + "os" + + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/recode/features" +) + +type UninstallResponse struct { + Error error + Content UninstallResponseContent +} + +type UninstallResponseContent struct { + RecodeAlreadyUninstalled bool + SuccessMessage string + AlreadyUninstalledMessage string + RecodeExecutablePath string + RecodeConfigDirPath string +} + +type UninstallPresenter interface { + PresentToView(UninstallResponse) +} + +type UninstallOutputHandler struct { + presenter UninstallPresenter +} + +func NewUninstallOutputHandler( + presenter UninstallPresenter, +) UninstallOutputHandler { + + return UninstallOutputHandler{ + presenter: presenter, + } +} + +func (u UninstallOutputHandler) HandleOutput(output features.UninstallOutput) error { + output.Stepper.StopCurrentStep() + + handleError := func(err error) error { + u.presenter.PresentToView(UninstallResponse{ + Error: err, + }) + + return err + } + + if output.Error != nil { + return handleError(output.Error) + } + + recodeExecutablePath, err := os.Executable() + + if err != nil { + return handleError(err) + } + + recodeConfigDirPath := system.UserConfigDir() + + u.presenter.PresentToView(UninstallResponse{ + Content: UninstallResponseContent{ + RecodeAlreadyUninstalled: output.Content.RecodeAlreadyUninstalled, + SuccessMessage: output.Content.SuccessMessage, + AlreadyUninstalledMessage: output.Content.AlreadyUninstalledMessage, + RecodeExecutablePath: recodeExecutablePath, + RecodeConfigDirPath: recodeConfigDirPath, + }, + }) + + return nil +} diff --git a/internal/hooks/pre_remove.go b/internal/hooks/pre_remove.go new file mode 100644 index 0000000..5bc6b93 --- /dev/null +++ b/internal/hooks/pre_remove.go @@ -0,0 +1,107 @@ +package hooks + +import ( + "encoding/json" + + "github.com/recode-sh/cli/internal/config" + cliEntities "github.com/recode-sh/cli/internal/entities" + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/recode/entities" +) + +type PreRemove struct { + sshConfig interfaces.SSHConfigManager + sshKeys interfaces.SSHKeysManager + sshKnownHosts interfaces.SSHKnownHostsManager + userConfig interfaces.UserConfigManager + github interfaces.GitHubManager +} + +func NewPreRemove( + sshConfig interfaces.SSHConfigManager, + sshKeys interfaces.SSHKeysManager, + sshKnownHosts interfaces.SSHKnownHostsManager, + userConfig interfaces.UserConfigManager, + github interfaces.GitHubManager, +) PreRemove { + + return PreRemove{ + sshConfig: sshConfig, + sshKeys: sshKeys, + sshKnownHosts: sshKnownHosts, + userConfig: userConfig, + github: github, + } +} + +func (p PreRemove) Run( + cloudService entities.CloudService, + recodeConfig *entities.Config, + cluster *entities.Cluster, + devEnv *entities.DevEnv, +) error { + + err := p.sshKeys.RemovePEMIfExists(devEnv.GetSSHKeyPairName()) + + if err != nil { + return err + } + + sshConfigHostKey := devEnv.Name + err = p.sshConfig.RemoveHostIfExists(sshConfigHostKey) + + if err != nil { + return err + } + + sshHostname := devEnv.InstancePublicIPAddress + err = p.sshKnownHosts.RemoveIfExists(sshHostname) + + if err != nil { + return err + } + + // User could remove dev env in creating state + // (in case of error for example) + if len(devEnv.AdditionalPropertiesJSON) == 0 { + return nil + } + + var devEnvAdditionalProperties *cliEntities.DevEnvAdditionalProperties + err = json.Unmarshal( + []byte(devEnv.AdditionalPropertiesJSON), + &devEnvAdditionalProperties, + ) + + if err != nil { + return err + } + + githubAccessToken := p.userConfig.GetString( + config.UserConfigKeyGitHubAccessToken, + ) + + if devEnvAdditionalProperties.GitHubCreatedSSHKeyId != nil { + err = p.github.RemoveSSHKey( + githubAccessToken, + *devEnvAdditionalProperties.GitHubCreatedSSHKeyId, + ) + + if err != nil && !p.github.IsNotFoundError(err) { + return err + } + } + + if devEnvAdditionalProperties.GitHubCreatedGPGKeyId != nil { + err = p.github.RemoveGPGKey( + githubAccessToken, + *devEnvAdditionalProperties.GitHubCreatedGPGKeyId, + ) + + if err != nil && !p.github.IsNotFoundError(err) { + return err + } + } + + return nil +} diff --git a/internal/hooks/pre_stop.go b/internal/hooks/pre_stop.go new file mode 100644 index 0000000..5c44964 --- /dev/null +++ b/internal/hooks/pre_stop.go @@ -0,0 +1,31 @@ +package hooks + +import ( + "github.com/recode-sh/cli/internal/interfaces" + "github.com/recode-sh/recode/entities" +) + +type PreStop struct { + sshKnownHosts interfaces.SSHKnownHostsManager +} + +func NewPreStop( + sshKnownHosts interfaces.SSHKnownHostsManager, +) PreStop { + + return PreStop{ + sshKnownHosts: sshKnownHosts, + } +} + +func (p PreStop) Run( + cloudService entities.CloudService, + recodeConfig *entities.Config, + cluster *entities.Cluster, + devEnv *entities.DevEnv, +) error { + + instanceIPAddress := devEnv.InstancePublicIPAddress + + return p.sshKnownHosts.RemoveIfExists(instanceIPAddress) +} diff --git a/internal/interfaces/browser.go b/internal/interfaces/browser.go new file mode 100644 index 0000000..7a8e1c2 --- /dev/null +++ b/internal/interfaces/browser.go @@ -0,0 +1,5 @@ +package interfaces + +type BrowserManager interface { + OpenURL(url string) error +} diff --git a/internal/interfaces/github.go b/internal/interfaces/github.go new file mode 100644 index 0000000..9e58206 --- /dev/null +++ b/internal/interfaces/github.go @@ -0,0 +1,22 @@ +package interfaces + +import ( + "github.com/google/go-github/v43/github" + cliGitHub "github.com/recode-sh/recode/github" +) + +type GitHubManager interface { + GetAuthenticatedUser(accessToken string) (*cliGitHub.AuthenticatedUser, error) + + CreateRepository(accessToken string, organization string, properties *github.Repository) (*github.Repository, error) + DoesRepositoryExist(accessToken, repositoryOwner, repositoryName string) (bool, error) + GetFileContentFromRepository(accessToken, repositoryOwner, repositoryName, filePath string) (string, error) + + CreateSSHKey(accessToken string, keyPairName string, publicKeyContent string) (*github.Key, error) + RemoveSSHKey(accessToken string, sshKeyID int64) error + + CreateGPGKey(accessToken string, publicKeyContent string) (*github.GPGKey, error) + RemoveGPGKey(accessToken string, gpgKeyID int64) error + + IsNotFoundError(err error) bool +} diff --git a/internal/interfaces/logger.go b/internal/interfaces/logger.go new file mode 100644 index 0000000..13702a2 --- /dev/null +++ b/internal/interfaces/logger.go @@ -0,0 +1,10 @@ +package interfaces + +type Logger interface { + Info(format string, v ...interface{}) + Warning(format string, v ...interface{}) + Error(format string, v ...interface{}) + Log(format string, v ...interface{}) + LogNoNewline(format string, v ...interface{}) + Write(p []byte) (n int, err error) +} diff --git a/internal/interfaces/sleeper.go b/internal/interfaces/sleeper.go new file mode 100644 index 0000000..604bcf7 --- /dev/null +++ b/internal/interfaces/sleeper.go @@ -0,0 +1,7 @@ +package interfaces + +import "time" + +type Sleeper interface { + Sleep(d time.Duration) +} diff --git a/internal/interfaces/ssh.go b/internal/interfaces/ssh.go new file mode 100644 index 0000000..1201e73 --- /dev/null +++ b/internal/interfaces/ssh.go @@ -0,0 +1,24 @@ +package interfaces + +// SSH known hosts + +type SSHKnownHostsManager interface { + RemoveIfExists(hostname string) error + AddOrReplace(hostname, algorithm, fingerprint string) error +} + +// SSH Config + +type SSHConfigManager interface { + AddOrReplaceHost(hostKey, hostName, identityFile, user string, port int64) error + UpdateHost(hostKey string, hostName, identityFile, user *string) error + RemoveHostIfExists(hostKey string) error +} + +// SSH keys + +type SSHKeysManager interface { + CreateOrReplacePEM(PEMName, PEMContent string) (string, error) + RemovePEMIfExists(PEMPath string) error + GetPEMFilePath(PEMName string) string +} diff --git a/internal/interfaces/user_config.go b/internal/interfaces/user_config.go new file mode 100644 index 0000000..dc31a5f --- /dev/null +++ b/internal/interfaces/user_config.go @@ -0,0 +1,14 @@ +package interfaces + +import ( + "github.com/recode-sh/cli/internal/config" + "github.com/recode-sh/recode/github" +) + +type UserConfigManager interface { + GetString(key config.UserConfigKey) string + GetBool(key config.UserConfigKey) bool + Set(key config.UserConfigKey, value interface{}) + WriteConfig() error + PopulateFromGitHubUser(githubUser *github.AuthenticatedUser) +} diff --git a/internal/interfaces/vscode.go b/internal/interfaces/vscode.go new file mode 100644 index 0000000..ecd4783 --- /dev/null +++ b/internal/interfaces/vscode.go @@ -0,0 +1,9 @@ +package interfaces + +type VSCodeProcessManager interface { + OpenOnRemote(hostKey, pathToOpen string) (cmdOutput string, cmdError error) +} + +type VSCodeExtensionsManager interface { + Install(extensionName string) (cmdOutput string, cmdError error) +} diff --git a/internal/mocks/aws_user_config_env_vars_resolver.go b/internal/mocks/aws_user_config_env_vars_resolver.go new file mode 100644 index 0000000..6c9f1e4 --- /dev/null +++ b/internal/mocks/aws_user_config_env_vars_resolver.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/recode-sh/cli/internal/aws (interfaces: UserConfigEnvVarsResolver) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + userconfig "github.com/recode-sh/aws-cloud-provider/userconfig" +) + +// AWSUserConfigEnvVarsResolver is a mock of UserConfigEnvVarsResolver interface. +type AWSUserConfigEnvVarsResolver struct { + ctrl *gomock.Controller + recorder *AWSUserConfigEnvVarsResolverMockRecorder +} + +// AWSUserConfigEnvVarsResolverMockRecorder is the mock recorder for AWSUserConfigEnvVarsResolver. +type AWSUserConfigEnvVarsResolverMockRecorder struct { + mock *AWSUserConfigEnvVarsResolver +} + +// NewAWSUserConfigEnvVarsResolver creates a new mock instance. +func NewAWSUserConfigEnvVarsResolver(ctrl *gomock.Controller) *AWSUserConfigEnvVarsResolver { + mock := &AWSUserConfigEnvVarsResolver{ctrl: ctrl} + mock.recorder = &AWSUserConfigEnvVarsResolverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *AWSUserConfigEnvVarsResolver) EXPECT() *AWSUserConfigEnvVarsResolverMockRecorder { + return m.recorder +} + +// Resolve mocks base method. +func (m *AWSUserConfigEnvVarsResolver) Resolve() (*userconfig.Config, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Resolve") + ret0, _ := ret[0].(*userconfig.Config) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Resolve indicates an expected call of Resolve. +func (mr *AWSUserConfigEnvVarsResolverMockRecorder) Resolve() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*AWSUserConfigEnvVarsResolver)(nil).Resolve)) +} diff --git a/internal/mocks/aws_user_config_files_resolver.go b/internal/mocks/aws_user_config_files_resolver.go new file mode 100644 index 0000000..3722274 --- /dev/null +++ b/internal/mocks/aws_user_config_files_resolver.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/recode-sh/cli/internal/aws (interfaces: UserConfigFilesResolver) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + userconfig "github.com/recode-sh/aws-cloud-provider/userconfig" +) + +// AWSUserConfigFilesResolver is a mock of UserConfigFilesResolver interface. +type AWSUserConfigFilesResolver struct { + ctrl *gomock.Controller + recorder *AWSUserConfigFilesResolverMockRecorder +} + +// AWSUserConfigFilesResolverMockRecorder is the mock recorder for AWSUserConfigFilesResolver. +type AWSUserConfigFilesResolverMockRecorder struct { + mock *AWSUserConfigFilesResolver +} + +// NewAWSUserConfigFilesResolver creates a new mock instance. +func NewAWSUserConfigFilesResolver(ctrl *gomock.Controller) *AWSUserConfigFilesResolver { + mock := &AWSUserConfigFilesResolver{ctrl: ctrl} + mock.recorder = &AWSUserConfigFilesResolverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *AWSUserConfigFilesResolver) EXPECT() *AWSUserConfigFilesResolverMockRecorder { + return m.recorder +} + +// Resolve mocks base method. +func (m *AWSUserConfigFilesResolver) Resolve() (*userconfig.Config, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Resolve") + ret0, _ := ret[0].(*userconfig.Config) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Resolve indicates an expected call of Resolve. +func (mr *AWSUserConfigFilesResolverMockRecorder) Resolve() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*AWSUserConfigFilesResolver)(nil).Resolve)) +} diff --git a/internal/mocks/views_displayer.go b/internal/mocks/views_displayer.go new file mode 100644 index 0000000..8fdc7fc --- /dev/null +++ b/internal/mocks/views_displayer.go @@ -0,0 +1,52 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/recode-sh/cli/internal/views (interfaces: Displayer) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + io "io" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockDisplayer is a mock of Displayer interface. +type MockDisplayer struct { + ctrl *gomock.Controller + recorder *MockDisplayerMockRecorder +} + +// MockDisplayerMockRecorder is the mock recorder for MockDisplayer. +type MockDisplayerMockRecorder struct { + mock *MockDisplayer +} + +// NewMockDisplayer creates a new mock instance. +func NewMockDisplayer(ctrl *gomock.Controller) *MockDisplayer { + mock := &MockDisplayer{ctrl: ctrl} + mock.recorder = &MockDisplayerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDisplayer) EXPECT() *MockDisplayerMockRecorder { + return m.recorder +} + +// Display mocks base method. +func (m *MockDisplayer) Display(arg0 io.Writer, arg1 string, arg2 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Display", varargs...) +} + +// Display indicates an expected call of Display. +func (mr *MockDisplayerMockRecorder) Display(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Display", reflect.TypeOf((*MockDisplayer)(nil).Display), varargs...) +} diff --git a/internal/presenters/errors.go b/internal/presenters/errors.go new file mode 100644 index 0000000..102b8b2 --- /dev/null +++ b/internal/presenters/errors.go @@ -0,0 +1,217 @@ +package presenters + +import ( + "errors" + "fmt" + "strings" + + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/exceptions" + "github.com/recode-sh/cli/internal/system" + "github.com/recode-sh/recode/entities" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type ViewableError struct { + Title string + Message string +} + +type ViewableErrorBuilder interface { + Build(error) *ViewableError +} + +type RecodeViewableErrorBuilder struct{} + +func NewRecodeViewableErrorBuilder() RecodeViewableErrorBuilder { + return RecodeViewableErrorBuilder{} +} + +func (RecodeViewableErrorBuilder) Build(err error) (viewableError *ViewableError) { + viewableError = &ViewableError{} + + if typedError, ok := err.(entities.ErrClusterNotExists); ok { + viewableError.Title = "Cluster not found" + viewableError.Message = fmt.Sprintf( + "The cluster \"%s\" was not found.", + typedError.ClusterName, + ) + + return + } + + if typedError, ok := err.(entities.ErrClusterAlreadyExists); ok { + viewableError.Title = "Cluster already exists" + viewableError.Message = fmt.Sprintf( + "The cluster \"%s\" already exists.", + typedError.ClusterName, + ) + + return + } + + if typedError, ok := err.(entities.ErrDevEnvNotExists); ok { + viewableError.Title = "Development environment not found" + + if typedError.ClusterName != entities.DefaultClusterName { + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" was not found in the cluster \"%s\".", + typedError.DevEnvName, + typedError.ClusterName, + ) + return + } + + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" was not found.", + typedError.DevEnvName, + ) + return + } + + if errors.Is(err, exceptions.ErrUserNotLoggedIn) { + viewableError.Title = "GitHub account not connected" + viewableError.Message = fmt.Sprintf( + "You must first connect your GitHub account using the command \"recode login\".\n\n"+ + "Recode requires the following permissions:\n\n"+ + " - \"Public SSH keys\" and \"Repositories\" to let you access your repositories from your development environments\n\n"+ + " - \"GPG Keys\" and \"Personal user data\" to configure Git and sign your commits (verified badge)\n\n"+ + "All your data (including the OAuth access token) will only be stored locally (in \"%s\").", + system.UserConfigFilePath(), + ) + + return + } + + if typedError, ok := err.(entities.ErrInvalidDevEnvUserConfig); ok { + viewableError.Title = "Invalid repository configuration" + viewableError.Message = fmt.Sprintf( + "The repository \"%s/%s\" contains an invalid configuration.\n\nReason: %s", + typedError.RepoOwner, + entities.DevEnvUserConfigRepoName, + typedError.Reason, + ) + + return + } + + if typedError, ok := err.(entities.ErrDevEnvRepositoryNotFound); ok { + viewableError.Title = "Repository not found" + viewableError.Message = fmt.Sprintf( + "The repository \"%s/%s\" was not found.\n\n"+ + "Please double check that this repository exists and that you can access it.", + typedError.RepoOwner, + typedError.RepoName, + ) + + return + } + + if typedError, ok := err.(entities.ErrStartRemovingDevEnv); ok { + viewableError.Title = "Invalid development environment state" + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" cannot be started because it is currently removing.\n\n"+ + "You must wait for the removing process to terminate.", + typedError.DevEnvName, + ) + + return + } + + if typedError, ok := err.(entities.ErrStartStoppingDevEnv); ok { + viewableError.Title = "Invalid development environment state" + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" cannot be started because it is currently stopping.\n\n"+ + "You must wait for the stopping process to terminate.", + typedError.DevEnvName, + ) + + return + } + + if typedError, ok := err.(entities.ErrStopRemovingDevEnv); ok { + viewableError.Title = "Invalid development environment state" + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" cannot be stopped because it is currently removing.\n\n"+ + "You must wait for the removing process to terminate.", + typedError.DevEnvName, + ) + + return + } + + if typedError, ok := err.(entities.ErrStopCreatingDevEnv); ok { + viewableError.Title = "Invalid development environment state" + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" cannot be stopped because it is currently creating.\n\n"+ + "You must wait for the creation process to terminate.", + typedError.DevEnvName, + ) + + return + } + + if typedError, ok := err.(entities.ErrStopStartingDevEnv); ok { + viewableError.Title = "Invalid development environment state" + viewableError.Message = fmt.Sprintf( + "The development environment \"%s\" cannot be stopped because it is currently starting.\n\n"+ + "You must wait for the starting process to terminate.", + typedError.DevEnvName, + ) + + return + } + + if typedError, ok := err.(exceptions.ErrLoginError); ok { + viewableError.Title = "GitHub connection error" + viewableError.Message = fmt.Sprintf( + "An error has occured during the authorization of the Recode application.\n\n%s", + typedError.Reason, + ) + + return + } + + if typedError, ok := err.(exceptions.ErrMissingRequirements); ok { + viewableError.Title = "Missing requirements" + viewableError.Message = fmt.Sprintf( + "The following requirements are missing:\n\n - %s", + strings.Join(typedError.MissingRequirements, "\n\n - "), + ) + + return + } + + bold := constants.Bold + + if status, ok := status.FromError(err); ok { + viewableError.Title = "Recode agent error" + + errorMessage := status.Message() + + if len(errorMessage) >= 2 { + errorMessage = strings.ToTitle(errorMessage[0:1]) + errorMessage[1:] + "." + } + + viewableError.Message = errorMessage + + if status.Code() != codes.Unknown { + viewableError.Message += "\n\n" + + bold("Error code: ") + + status.Code().String() + } + + return + } + + viewableError.Title = "Unknown error" + viewableError.Message = fmt.Sprintf( + "An unknown error occurred.\n\n"+ + "You could try to fix it (using the details below) and rerun the command or open a new issue at: https://github.com/recode-sh/cli/issues/new\n\n"+ + bold("%s"), + err.Error(), + ) + + return +} diff --git a/internal/presenters/login.go b/internal/presenters/login.go new file mode 100644 index 0000000..77f3bad --- /dev/null +++ b/internal/presenters/login.go @@ -0,0 +1,54 @@ +package presenters + +import ( + "github.com/recode-sh/cli/internal/features" +) + +type LoginViewDataContent struct { + Message string +} + +type LoginViewData struct { + Error *ViewableError + Content LoginViewDataContent +} + +type LoginViewer interface { + View(LoginViewData) +} + +type LoginPresenter struct { + viewableErrorBuilder ViewableErrorBuilder + viewer LoginViewer +} + +func NewLoginPresenter( + viewableErrorBuilder ViewableErrorBuilder, + viewer LoginViewer, +) LoginPresenter { + + return LoginPresenter{ + viewableErrorBuilder: viewableErrorBuilder, + viewer: viewer, + } +} + +func (l LoginPresenter) PresentToView(response features.LoginResponse) { + viewData := LoginViewData{} + + if response.Error == nil { + viewDataMessage := "Your GitHub account is now connected." + + viewData.Content = LoginViewDataContent{ + Message: viewDataMessage, + } + + l.viewer.View(viewData) + + return + } + + viewData.Error = l.viewableErrorBuilder.Build(response.Error) + + l.viewer.View(viewData) +} diff --git a/internal/presenters/remove.go b/internal/presenters/remove.go new file mode 100644 index 0000000..bd451e0 --- /dev/null +++ b/internal/presenters/remove.go @@ -0,0 +1,55 @@ +package presenters + +import ( + "github.com/recode-sh/cli/internal/features" +) + +type RemoveViewDataContent struct { + Message string +} + +type RemoveViewData struct { + Error *ViewableError + Content RemoveViewDataContent +} + +type RemoveViewer interface { + View(RemoveViewData) +} + +type RemovePresenter struct { + viewableErrorBuilder ViewableErrorBuilder + viewer RemoveViewer +} + +func NewRemovePresenter( + viewableErrorBuilder ViewableErrorBuilder, + viewer RemoveViewer, +) RemovePresenter { + + return RemovePresenter{ + viewableErrorBuilder: viewableErrorBuilder, + viewer: viewer, + } +} + +func (r RemovePresenter) PresentToView(response features.RemoveResponse) { + viewData := RemoveViewData{} + + if response.Error == nil { + devEnvName := response.Content.DevEnvName + viewDataMessage := "The development environment \"" + devEnvName + "\" was removed." + + viewData.Content = RemoveViewDataContent{ + Message: viewDataMessage, + } + + r.viewer.View(viewData) + + return + } + + viewData.Error = r.viewableErrorBuilder.Build(response.Error) + + r.viewer.View(viewData) +} diff --git a/internal/presenters/start.go b/internal/presenters/start.go new file mode 100644 index 0000000..a696474 --- /dev/null +++ b/internal/presenters/start.go @@ -0,0 +1,76 @@ +package presenters + +import ( + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/features" +) + +type StartViewData struct { + Error *ViewableError + Content StartViewDataContent +} + +type StartViewDataContent struct { + ShowAsWarning bool + Message string + Subtext string +} + +type StartViewer interface { + View(StartViewData) +} + +type StartPresenter struct { + viewableErrorBuilder ViewableErrorBuilder + viewer StartViewer +} + +func NewStartPresenter( + viewableErrorBuilder ViewableErrorBuilder, + viewer StartViewer, +) StartPresenter { + + return StartPresenter{ + viewableErrorBuilder: viewableErrorBuilder, + viewer: viewer, + } +} + +func (s StartPresenter) PresentToView(response features.StartResponse) { + viewData := StartViewData{} + + if response.Error == nil { + devEnvName := response.Content.DevEnvName + + viewDataMessage := "The development environment \"" + devEnvName + "\" was started." + viewDataSubtext := "Run `" + constants.Blue("ssh "+devEnvName) + "` (or use your code editor's integrated terminal)." + + devEnvAlreadyStarted := response.Content.DevEnvAlreadyStarted + + if devEnvAlreadyStarted { + viewDataMessage = "The development environment \"" + devEnvName + "\" is already started." + viewDataSubtext = "" + } + + devEnvRebuilt := response.Content.DevEnvRebuilt + + if devEnvRebuilt { + viewDataMessage = "The development environment \"" + devEnvName + "\" was rebuilt." + viewDataSubtext = "" + } + + viewData.Content = StartViewDataContent{ + ShowAsWarning: devEnvAlreadyStarted, + Message: viewDataMessage, + Subtext: viewDataSubtext, + } + + s.viewer.View(viewData) + + return + } + + viewData.Error = s.viewableErrorBuilder.Build(response.Error) + + s.viewer.View(viewData) +} diff --git a/internal/presenters/stop.go b/internal/presenters/stop.go new file mode 100644 index 0000000..884f989 --- /dev/null +++ b/internal/presenters/stop.go @@ -0,0 +1,63 @@ +package presenters + +import ( + "github.com/recode-sh/cli/internal/features" +) + +type StopViewDataContent struct { + ShowAsWarning bool + Message string +} + +type StopViewData struct { + Error *ViewableError + Content StopViewDataContent +} + +type StopViewer interface { + View(StopViewData) +} + +type StopPresenter struct { + viewableErrorBuilder ViewableErrorBuilder + viewer StopViewer +} + +func NewStopPresenter( + viewableErrorBuilder ViewableErrorBuilder, + viewer StopViewer, +) StopPresenter { + + return StopPresenter{ + viewableErrorBuilder: viewableErrorBuilder, + viewer: viewer, + } +} + +func (s StopPresenter) PresentToView(response features.StopResponse) { + viewData := StopViewData{} + + if response.Error == nil { + devEnvName := response.Content.DevEnvName + devEnvAlreadyStopped := response.Content.DevEnvAlreadyStopped + + viewDataMessage := "The development environment \"" + devEnvName + "\" was stopped." + + if devEnvAlreadyStopped { + viewDataMessage = "The development environment \"" + devEnvName + "\" is already stopped. Nothing to do." + } + + viewData.Content = StopViewDataContent{ + ShowAsWarning: devEnvAlreadyStopped, + Message: viewDataMessage, + } + + s.viewer.View(viewData) + + return + } + + viewData.Error = s.viewableErrorBuilder.Build(response.Error) + + s.viewer.View(viewData) +} diff --git a/internal/presenters/uninstall.go b/internal/presenters/uninstall.go new file mode 100644 index 0000000..f6bd114 --- /dev/null +++ b/internal/presenters/uninstall.go @@ -0,0 +1,77 @@ +package presenters + +import ( + "fmt" + + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/features" +) + +type UninstallViewDataContent struct { + ShowAsWarning bool + Message string + Subtext string +} + +type UninstallViewData struct { + Error *ViewableError + Content UninstallViewDataContent +} + +type UninstallViewer interface { + View(UninstallViewData) +} + +type UninstallPresenter struct { + viewableErrorBuilder ViewableErrorBuilder + viewer UninstallViewer +} + +func NewUninstallPresenter( + viewableErrorBuilder ViewableErrorBuilder, + viewer UninstallViewer, +) UninstallPresenter { + + return UninstallPresenter{ + viewableErrorBuilder: viewableErrorBuilder, + viewer: viewer, + } +} + +func (u UninstallPresenter) PresentToView(response features.UninstallResponse) { + viewData := UninstallViewData{} + + if response.Error == nil { + bold := constants.Bold + + recodeAlreadyUninstalled := response.Content.RecodeAlreadyUninstalled + + viewDataMessage := response.Content.SuccessMessage + viewDataSubtext := fmt.Sprintf( + "If you want to remove Recode entirely:\n\n"+ + " - Remove the binary located at path %s\n\n"+ + " - Remove the contiguration located at path %s\n\n"+ + " - Unauthorize the application on GitHub by going to %s", + bold("\""+response.Content.RecodeExecutablePath+"\""), + bold("\""+response.Content.RecodeConfigDirPath+"\""), + bold("https://github.com/settings/applications"), + ) + + if recodeAlreadyUninstalled { + viewDataMessage = response.Content.AlreadyUninstalledMessage + } + + viewData.Content = UninstallViewDataContent{ + ShowAsWarning: recodeAlreadyUninstalled, + Message: viewDataMessage, + Subtext: viewDataSubtext, + } + + u.viewer.View(viewData) + + return + } + + viewData.Error = u.viewableErrorBuilder.Build(response.Error) + u.viewer.View(viewData) +} diff --git a/internal/ssh/config.go b/internal/ssh/config.go new file mode 100644 index 0000000..db30526 --- /dev/null +++ b/internal/ssh/config.go @@ -0,0 +1,214 @@ +package ssh + +import ( + "fmt" + "os" + "strings" + + "github.com/kevinburke/ssh_config" + "github.com/recode-sh/cli/internal/system" +) + +const ConfigFilePerm os.FileMode = 0644 + +type Config struct { + configFilePath string +} + +func NewConfig(configFilePath string) Config { + return Config{ + configFilePath: configFilePath, + } +} + +func NewConfigWithDefaultConfigFilePath() Config { + return NewConfig(system.DefaultSSHConfigFilePath()) +} + +func (c Config) AddOrReplaceHost( + hostKey string, + hostName string, + identityFile string, + user string, + port int64, +) error { + + cfg, err := c.parse() + + if err != nil { + return err + } + + hostPattern, err := ssh_config.NewPattern(hostKey) + + if err != nil { + return err + } + + hostNodes := []ssh_config.Node{ + &ssh_config.Empty{ + Comment: " added by Recode", + }, + + &ssh_config.KV{ + Key: " HostName", + Value: hostName, + }, + + &ssh_config.KV{ + Key: " IdentityFile", + Value: identityFile, + }, + + &ssh_config.KV{ + Key: " User", + Value: user, + }, + + &ssh_config.KV{ + Key: " Port", + Value: fmt.Sprint(port), + }, + + &ssh_config.KV{ + Key: " ForwardAgent", + Value: "yes", + }, + } + + hostToAdd := &ssh_config.Host{ + Patterns: []*ssh_config.Pattern{ + hostPattern, + }, + Nodes: hostNodes, + } + + hostToAddIndex := c.lookupHostIndex( + cfg, + hostKey, + ) + + if hostToAddIndex == -1 { + cfg.Hosts = append(cfg.Hosts, hostToAdd) + } else { + cfg.Hosts[hostToAddIndex] = hostToAdd + } + + return c.save(cfg) +} + +func (c Config) UpdateHost( + hostKey string, + hostName *string, + identityFile *string, + user *string, +) error { + + cfg, err := c.parse() + + if err != nil { + return err + } + + updatedHosts := []*ssh_config.Host{} + + for _, host := range cfg.Hosts { + // We don't use "host.Matches()" + // here because we don't want the + // wildcard host ("Host *") to match + if len(host.Patterns) == 1 && host.Patterns[0].String() == hostKey { + for _, node := range host.Nodes { + switch t := node.(type) { + case *ssh_config.KV: + lowercasedKey := strings.ToLower(t.Key) + + if lowercasedKey == "hostname" && hostName != nil { + t.Value = *hostName + } + + if lowercasedKey == "identityfile" && identityFile != nil { + t.Value = *identityFile + } + + if lowercasedKey == "user" && user != nil { + t.Value = *user + } + } + } + } + + updatedHosts = append(updatedHosts, host) + } + + cfg.Hosts = updatedHosts + + return c.save(cfg) +} + +func (c Config) RemoveHostIfExists(hostKey string) error { + cfg, err := c.parse() + + if err != nil { + return err + } + + updatedHosts := []*ssh_config.Host{} + + for _, host := range cfg.Hosts { + // We don't use "host.Matches()" + // here because we don't want the + // wildcard host ("Host *") to match + if len(host.Patterns) == 1 && host.Patterns[0].String() == hostKey { + continue + } + + updatedHosts = append(updatedHosts, host) + } + + cfg.Hosts = updatedHosts + + return c.save(cfg) +} + +func (c Config) lookupHostIndex( + cfg *ssh_config.Config, + hostKey string, +) int { + + for hostIndex, host := range cfg.Hosts { + // We don't use "host.Matches()" + // here because we don't want the + // wildcard host ("Host *") to match + if len(host.Patterns) == 1 && host.Patterns[0].String() == hostKey { + return hostIndex + } + + continue + } + + return -1 +} + +func (c Config) parse() (*ssh_config.Config, error) { + f, err := os.OpenFile( + c.configFilePath, + os.O_CREATE|os.O_RDONLY, + ConfigFilePerm, + ) + + if err != nil { + return nil, err + } + + defer f.Close() + + return ssh_config.Decode(f) +} + +func (c Config) save(cfg *ssh_config.Config) error { + return os.WriteFile( + c.configFilePath, + []byte(cfg.String()), + ConfigFilePerm, + ) +} diff --git a/internal/ssh/config_add_host_test.go b/internal/ssh/config_add_host_test.go new file mode 100644 index 0000000..84f04d0 --- /dev/null +++ b/internal/ssh/config_add_host_test.go @@ -0,0 +1,264 @@ +package ssh_test + +import ( + "os" + "strings" + "testing" + + "github.com/recode-sh/cli/internal/ssh" + "github.com/recode-sh/cli/internal/system" +) + +func TestConfigAddOrReplaceHostWithNonEmptyConfig(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := string(configAtStart) + ` +Host hostkey +# added by Recode + HostName hostname + IdentityFile identityFile + User user + Port 2200 + ForwardAgent yes` + + config := ssh.NewConfig(configPath) + err = config.AddOrReplaceHost( + "hostkey", + "hostname", + "identityFile", + "user", + 2200, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} + +func TestConfigAddOrReplaceHostWithEmptyConfig(t *testing.T) { + configPath := "./testdata/empty_ssh_config" + expectedConfig := `Host hostkey +# added by Recode + HostName hostname + IdentityFile identityFile + User user + Port 2200 + ForwardAgent yes` + + defer func() { // Reset modified config file + err := os.WriteFile( + configPath, + []byte(""), + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + config := ssh.NewConfig(configPath) + err := config.AddOrReplaceHost( + "hostkey", + "hostname", + "identityFile", + "user", + 2200, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} + +func TestConfigAddOrReplaceHostWithNonExistingConfig(t *testing.T) { + configPath := "./testdata/non_existing_ssh_config" + expectedConfig := `Host hostkey +# added by Recode + HostName hostname + IdentityFile identityFile + User user + Port 2200 + ForwardAgent yes` + + defer func() { // Remove created config file + err := os.Remove(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + config := ssh.NewConfig(configPath) + err := config.AddOrReplaceHost( + "hostkey", + "hostname", + "identityFile", + "user", + 2200, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} + +func TestConfigAddOrReplaceHostWithInvalidHostKey(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + invalidHostKey := "" + + config := ssh.NewConfig(configPath) + err := config.AddOrReplaceHost( + invalidHostKey, + "hostname", + "identityFile", + "user", + 2200, + ) + + if err == nil { + t.Fatalf("expected error, got nothing") + } +} + +func TestConfigAddOrReplaceHostWithExistingHostConfig(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := `Host * + AddKeysToAgent yes + UseKeychain yes + IdentityFile ~/.ssh/id_rsa + IdentitiesOnly yes + ServerAliveInterval 240 + +Host 34.128.204.12 +# added by Recode + HostName hostname_replaced + IdentityFile identityFile_replaced + User user_replaced + Port 2200 + ForwardAgent yes` + + config := ssh.NewConfig(configPath) + err = config.AddOrReplaceHost( + "34.128.204.12", + "hostname_replaced", + "identityFile_replaced", + "user_replaced", + 2200, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} diff --git a/internal/ssh/config_remove_host_test.go b/internal/ssh/config_remove_host_test.go new file mode 100644 index 0000000..12d4766 --- /dev/null +++ b/internal/ssh/config_remove_host_test.go @@ -0,0 +1,114 @@ +package ssh_test + +import ( + "os" + "strings" + "testing" + + "github.com/recode-sh/cli/internal/ssh" + "github.com/recode-sh/cli/internal/system" +) + +func TestConfigRemoveHostWithExistingHost(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := `Host * + AddKeysToAgent yes + UseKeychain yes + IdentityFile ~/.ssh/id_rsa + IdentitiesOnly yes + ServerAliveInterval 240 +` + + config := ssh.NewConfig(configPath) + err = config.RemoveHostIfExists("34.128.204.12") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} + +func TestConfigRemoveHostWithNonExistingHost(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := string(configAtStart) + + config := ssh.NewConfig(configPath) + err = config.RemoveHostIfExists("34.228.204.12") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} diff --git a/internal/ssh/config_update_host_test.go b/internal/ssh/config_update_host_test.go new file mode 100644 index 0000000..bab63bb --- /dev/null +++ b/internal/ssh/config_update_host_test.go @@ -0,0 +1,207 @@ +package ssh_test + +import ( + "os" + "strings" + "testing" + + "github.com/recode-sh/cli/internal/ssh" + "github.com/recode-sh/cli/internal/system" +) + +func TestConfigUpdateHostWithExistingHost(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := `Host * + AddKeysToAgent yes + UseKeychain yes + IdentityFile ~/.ssh/id_rsa + IdentitiesOnly yes + ServerAliveInterval 240 + +Host 34.128.204.12 + HostName updated_hostname + IdentityFile updated_identityFile + User updated_user + ForwardAgent yes` + + config := ssh.NewConfig(configPath) + + updatedHostName := "updated_hostname" + updatedUser := "updated_user" + updatedIdentityFile := "updated_identityFile" + + err = config.UpdateHost( + "34.128.204.12", + &updatedHostName, + &updatedIdentityFile, + &updatedUser, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} + +func TestConfigUpdateHostWithExistingHostAndPartialConfig(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := `Host * + AddKeysToAgent yes + UseKeychain yes + IdentityFile ~/.ssh/id_rsa + IdentitiesOnly yes + ServerAliveInterval 240 + +Host 34.128.204.12 + HostName updated_hostname + IdentityFile identityFile + User user + ForwardAgent yes` + + config := ssh.NewConfig(configPath) + + updatedHostName := "updated_hostname" + + err = config.UpdateHost( + "34.128.204.12", + &updatedHostName, + nil, + nil, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} + +func TestConfigUpdateHostWithNonExistingHost(t *testing.T) { + configPath := "./testdata/non_empty_ssh_config" + configAtStart, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified config file + err = os.WriteFile( + configPath, + configAtStart, + ssh.ConfigFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedConfig := string(configAtStart) + + config := ssh.NewConfig(configPath) + + updatedHostName := "updated_hostname" + updatedUser := "updated_user" + updatedIdentityFile := "updated_identityFile" + + err = config.UpdateHost( + "34.228.204.12", + &updatedHostName, + &updatedIdentityFile, + &updatedUser, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEnd, err := os.ReadFile(configPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + configAtEndString := strings.TrimSuffix( + string(configAtEnd), + system.NewLineChar, + ) + + if configAtEndString != expectedConfig { + t.Fatalf( + "expected config to equal '%s', got '%s'", + expectedConfig, + configAtEndString, + ) + } +} diff --git a/internal/ssh/keys.go b/internal/ssh/keys.go new file mode 100644 index 0000000..feb6f58 --- /dev/null +++ b/internal/ssh/keys.go @@ -0,0 +1,63 @@ +package ssh + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + + "github.com/recode-sh/cli/internal/system" +) + +const PrivateKeyFilePerm os.FileMode = 0600 + +type Keys struct { + sshDir string +} + +func NewKeys(SSHDir string) Keys { + return Keys{ + sshDir: SSHDir, + } +} + +func NewKeysWithDefaultDir() Keys { + return NewKeys( + system.DefaultSSHDir(), + ) +} + +func (k Keys) CreateOrReplacePEM( + PEMName string, + PEMContent string, +) (pathWritten string, err error) { + + pathWritten = filepath.Join(k.sshDir, PEMName+".pem") + + err = os.WriteFile( + pathWritten, + []byte(PEMContent), + PrivateKeyFilePerm, + ) + + return +} + +func (k Keys) RemovePEMIfExists(PEMName string) error { + err := os.Remove( + k.GetPEMFilePath(PEMName), + ) + + if err != nil && errors.Is(err, fs.ErrNotExist) { + return nil + } + + return err +} + +func (k Keys) GetPEMFilePath(PEMName string) string { + return filepath.Join( + k.sshDir, + PEMName+".pem", + ) +} diff --git a/internal/ssh/keys_add_pem_test.go b/internal/ssh/keys_add_pem_test.go new file mode 100644 index 0000000..c1dfb3b --- /dev/null +++ b/internal/ssh/keys_add_pem_test.go @@ -0,0 +1,69 @@ +package ssh_test + +import ( + "os" + "testing" + + "github.com/recode-sh/cli/internal/ssh" +) + +func TestKeysCreateOrReplacePEM(t *testing.T) { + keys := ssh.NewKeys("./testdata") + + expectedPEMName := "pem_name" + expectedPEMContent := "pem_content" + expectedPEMPath := "./testdata/" + expectedPEMName + ".pem" + + PEMPath, err := keys.CreateOrReplacePEM( + expectedPEMName, + expectedPEMContent, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Remove created PEM file + err = os.Remove(PEMPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + if "./"+PEMPath != expectedPEMPath { + t.Fatalf( + "expected PEM path to equal '%s', got '%s'", + expectedPEMPath, + PEMPath, + ) + } + + createdPEMContent, err := os.ReadFile(expectedPEMPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(createdPEMContent) != expectedPEMContent { + t.Fatalf( + "expected PEM to equal '%s', got '%s'", + expectedPEMContent, + string(createdPEMContent), + ) + } + + createdPEMFileInfo, err := os.Stat(expectedPEMPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if createdPEMFileInfo.Mode().Perm() != ssh.PrivateKeyFilePerm { + t.Fatalf( + "expected created PEM file to have permission '%o', got '%o'", + ssh.PrivateKeyFilePerm, + createdPEMFileInfo.Mode().Perm(), + ) + } +} diff --git a/internal/ssh/keys_remove_pem_test.go b/internal/ssh/keys_remove_pem_test.go new file mode 100644 index 0000000..c2522e7 --- /dev/null +++ b/internal/ssh/keys_remove_pem_test.go @@ -0,0 +1,47 @@ +package ssh_test + +import ( + "errors" + "os" + "testing" + + "github.com/recode-sh/cli/internal/ssh" +) + +func TestKeysRemoveExistingPEM(t *testing.T) { + keys := ssh.NewKeys("./testdata") + + PEMName := "pem_to_remove" + PEMPath := "./testdata/" + PEMName + ".pem" + + _, err := os.Create(PEMPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + err = keys.RemovePEMIfExists(PEMName) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + _, err = os.Stat(PEMPath) + + if err == nil { + t.Fatalf("expected file not exists error, got nothing") + } + + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected file not exists error, got '%+v'", err) + } +} + +func TestKeysRemoveNonExistingPEM(t *testing.T) { + keys := ssh.NewKeys("./testdata") + err := keys.RemovePEMIfExists("non_existing_pem") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } +} diff --git a/internal/ssh/known_hosts.go b/internal/ssh/known_hosts.go new file mode 100644 index 0000000..a1029a4 --- /dev/null +++ b/internal/ssh/known_hosts.go @@ -0,0 +1,137 @@ +package ssh + +import ( + "bufio" + "os" + "strings" + + "github.com/recode-sh/cli/internal/system" +) + +const KnownHostsFilePerm os.FileMode = 0644 + +type KnownHosts struct { + knownHostsFilePath string +} + +func NewKnownHosts(knownHostsFilePath string) KnownHosts { + return KnownHosts{ + knownHostsFilePath: knownHostsFilePath, + } +} + +func NewKnownHostsWithDefaultKnownHostsFilePath() KnownHosts { + return NewKnownHosts( + system.DefaultSSHKnownHostsFilePath(), + ) +} + +func (k KnownHosts) AddOrReplace(hostname, algorithm, fingerprint string) error { + f, err := k.openFile() + + if err != nil { + return err + } + + defer f.Close() + + knownHostToAdd := hostname + " " + algorithm + " " + fingerprint + knownHostToAddReplaced := false + + scanner := bufio.NewScanner(f) + newKnownHostsContent := "" + + for scanner.Scan() { + knownHostLine := scanner.Text() + + if strings.HasPrefix(knownHostLine, hostname+" "+algorithm) { + newKnownHostsContent += knownHostToAdd + system.NewLineChar + knownHostToAddReplaced = true + continue + } + + newKnownHostsContent += knownHostLine + system.NewLineChar + } + + if err := scanner.Err(); err != nil { + return err + } + + if !knownHostToAddReplaced { + newKnownHostsContent += knownHostToAdd + system.NewLineChar + } + + return os.WriteFile( + k.knownHostsFilePath, + []byte(newKnownHostsContent), + KnownHostsFilePerm, + ) +} + +func (k KnownHosts) RemoveIfExists(hostname string) error { + if len(hostname) == 0 { + // Nothing to do. + // We don't want to remove all hostnames + // (the function "hasPrefix" will always return "true" if prefix is empty). + // See below. + return nil + } + + f, err := k.openFile() + + if err != nil { + return err + } + + defer f.Close() + + scanner := bufio.NewScanner(f) + newKnownHostsContent := "" + + for scanner.Scan() { + knownHostLine := scanner.Text() + + if strings.HasPrefix(knownHostLine, hostname) { + continue + } + + newKnownHostsContent += knownHostLine + system.NewLineChar + } + + if err := scanner.Err(); err != nil { + return err + } + + /* We only want one new line + at the end of the file */ + + for { + trimedNewKnownHostsContent := strings.TrimSuffix( + newKnownHostsContent, + system.NewLineChar, + ) + + if trimedNewKnownHostsContent == newKnownHostsContent { + break + } + + newKnownHostsContent = trimedNewKnownHostsContent + } + + newKnownHostsContent += system.NewLineChar + + return os.WriteFile( + k.knownHostsFilePath, + []byte(newKnownHostsContent), + KnownHostsFilePerm, + ) +} + +func (k KnownHosts) openFile() (*os.File, error) { + // create the "known_hosts" file if necessary + return os.OpenFile( + k.knownHostsFilePath, + os.O_APPEND|os.O_CREATE|os.O_RDWR, + KnownHostsFilePerm, + ) +} diff --git a/internal/ssh/known_hosts_add_test.go b/internal/ssh/known_hosts_add_test.go new file mode 100644 index 0000000..b9a79c4 --- /dev/null +++ b/internal/ssh/known_hosts_add_test.go @@ -0,0 +1,202 @@ +package ssh_test + +import ( + "os" + "testing" + + "github.com/recode-sh/cli/internal/ssh" + "github.com/recode-sh/cli/internal/system" +) + +func TestKnownHostsAddOrReplaceWithNonEmptyKnownHostsFile(t *testing.T) { + knownHostsPath := "./testdata/non_empty_known_hosts" + knownHostsAtStart, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified known hosts file + err = os.WriteFile( + knownHostsPath, + knownHostsAtStart, + os.FileMode(ssh.KnownHostsFilePerm), + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedKnownHosts := string(knownHostsAtStart) + + "hostname algorithm fingerprint" + + system.NewLineChar + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + err = knownHosts.AddOrReplace("hostname", "algorithm", "fingerprint") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(knownHostsAtEnd) != expectedKnownHosts { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + expectedKnownHosts, + string(knownHostsAtEnd), + ) + } +} + +func TestKnownHostsAddOrReplaceWithEmptyKnownHostsFile(t *testing.T) { + knownHostsPath := "./testdata/empty_known_hosts" + expectedKnownHosts := + "hostname algorithm fingerprint" + + system.NewLineChar + + defer func() { // Reset modified known hosts file + err := os.WriteFile( + knownHostsPath, + []byte(""), + os.FileMode(ssh.KnownHostsFilePerm), + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + err := knownHosts.AddOrReplace("hostname", "algorithm", "fingerprint") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(knownHostsAtEnd) != expectedKnownHosts { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + expectedKnownHosts, + string(knownHostsAtEnd), + ) + } +} + +func TestKnownHostsAddOrReplaceWithNonExistingKnownHostsFile(t *testing.T) { + knownHostsPath := "./testdata/non_existing_known_hosts" + expectedKnownHosts := + "hostname algorithm fingerprint" + + system.NewLineChar + + defer func() { // Remove created known hosts file + err := os.Remove(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + err := knownHosts.AddOrReplace("hostname", "algorithm", "fingerprint") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(knownHostsAtEnd) != expectedKnownHosts { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + expectedKnownHosts, + string(knownHostsAtEnd), + ) + } + + createdKnownHostsFileInfo, err := os.Stat(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if createdKnownHostsFileInfo.Mode().Perm() != ssh.KnownHostsFilePerm { + t.Fatalf( + "expected created known hosts file to have permission '%o', got '%o'", + ssh.KnownHostsFilePerm, + createdKnownHostsFileInfo.Mode().Perm(), + ) + } +} + +func TestKnownHostsAddOrReplaceWitExistingHost(t *testing.T) { + knownHostsPath := "./testdata/non_empty_known_hosts" + knownHostsAtStart, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified known hosts file + err = os.WriteFile( + knownHostsPath, + knownHostsAtStart, + os.FileMode(ssh.KnownHostsFilePerm), + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedKnownHosts := `github.com,140.82.118.3 ssh-rsa fingerprint +34.229.126.51 ssh-rsa fingerprint + +github.com ecdsa-sha2-nistp256 fingerprint +github.com ssh-ed25519 fingerprint + +34.229.126.51 ecdsa-sha2-nistp256 fingerprint_replaced +34.229.126.51 ssh-ed25519 fingerprint +` + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + + err = knownHosts.AddOrReplace( + "34.229.126.51", + "ecdsa-sha2-nistp256", + "fingerprint_replaced", + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(knownHostsAtEnd) != expectedKnownHosts { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + expectedKnownHosts, + string(knownHostsAtEnd), + ) + } +} diff --git a/internal/ssh/known_hosts_remove_test.go b/internal/ssh/known_hosts_remove_test.go new file mode 100644 index 0000000..fe9600a --- /dev/null +++ b/internal/ssh/known_hosts_remove_test.go @@ -0,0 +1,144 @@ +package ssh_test + +import ( + "os" + "testing" + + "github.com/recode-sh/cli/internal/ssh" +) + +func TestKnownHostsRemoveWithExistingHostname(t *testing.T) { + knownHostsPath := "./testdata/non_empty_known_hosts" + knownHostsAtStart, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified known hosts file + err = os.WriteFile( + knownHostsPath, + knownHostsAtStart, + ssh.KnownHostsFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedKnownHosts := `github.com,140.82.118.3 ssh-rsa fingerprint + +github.com ecdsa-sha2-nistp256 fingerprint +github.com ssh-ed25519 fingerprint +` + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + err = knownHosts.RemoveIfExists("34.229.126.51") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(knownHostsAtEnd) != expectedKnownHosts { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + expectedKnownHosts, + string(knownHostsAtEnd), + ) + } +} + +func TestKnownHostsRemoveWithNonExistingHostname(t *testing.T) { + knownHostsPath := "./testdata/non_empty_known_hosts" + knownHostsAtStart, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified known hosts file + err = os.WriteFile( + knownHostsPath, + knownHostsAtStart, + ssh.KnownHostsFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedKnownHosts := knownHostsAtStart + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + err = knownHosts.RemoveIfExists("104.78.1.4") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(expectedKnownHosts) != string(knownHostsAtEnd) { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + string(expectedKnownHosts), + string(knownHostsAtEnd), + ) + } +} + +func TestKnownHostsRemoveWithEmptyHostname(t *testing.T) { + knownHostsPath := "./testdata/non_empty_known_hosts" + knownHostsAtStart, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + defer func() { // Reset modified known hosts file + err = os.WriteFile( + knownHostsPath, + knownHostsAtStart, + ssh.KnownHostsFilePerm, + ) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + }() + + expectedKnownHosts := knownHostsAtStart + + knownHosts := ssh.NewKnownHosts(knownHostsPath) + err = knownHosts.RemoveIfExists("") + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + knownHostsAtEnd, err := os.ReadFile(knownHostsPath) + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if string(expectedKnownHosts) != string(knownHostsAtEnd) { + t.Fatalf( + "expected known hosts to equal '%s', got '%s'", + string(expectedKnownHosts), + string(knownHostsAtEnd), + ) + } +} diff --git a/internal/ssh/port_forwarding.go b/internal/ssh/port_forwarding.go new file mode 100644 index 0000000..0bb5f37 --- /dev/null +++ b/internal/ssh/port_forwarding.go @@ -0,0 +1,152 @@ +package ssh + +import ( + "io" + "net" + "time" + + "golang.org/x/crypto/ssh" +) + +type PortForwarder struct{} + +func NewPortForwarder() PortForwarder { + return PortForwarder{} +} + +type PortForwarderReadyResp struct { + Error error + LocalAddr string +} + +func (p PortForwarder) Forward( + onReadyChan chan<- PortForwarderReadyResp, + privateKeyBytes []byte, + user string, + serverAddr string, + localAddr string, + remoteAddrProtocol string, + remoteAddr string, +) error { + + sshConfig, err := p.buildSSHConfig( + 8*time.Second, + user, + privateKeyBytes, + ) + + if err != nil { + onReadyChan <- PortForwarderReadyResp{ + Error: err, + } + return nil + } + + // Establish connection with server through SSH + serverSSHConn, err := ssh.Dial("tcp", serverAddr, sshConfig) + + if err != nil { + onReadyChan <- PortForwarderReadyResp{ + Error: err, + } + return nil + } + + defer serverSSHConn.Close() + + // Establish connection with remoteAddr from server + remoteConn, err := serverSSHConn.Dial(remoteAddrProtocol, remoteAddr) + + if err != nil { + onReadyChan <- PortForwarderReadyResp{ + Error: err, + } + return nil + } + + // Start local TCP server to forward traffic to remote connection + localTCPServer, err := net.Listen("tcp", localAddr) + + if err != nil { + onReadyChan <- PortForwarderReadyResp{ + Error: err, + } + return nil + } + + defer localTCPServer.Close() + + onReadyChan <- PortForwarderReadyResp{ + LocalAddr: localTCPServer.Addr().String(), + } + + localConn, err := localTCPServer.Accept() + + if err != nil { + return err + } + + return p.forwardLocalToRemoteConn( + localConn, + remoteConn, + ) +} + +// Get ssh client config for our connection +func (p PortForwarder) buildSSHConfig( + connTimeout time.Duration, + user string, + privateKeyBytes []byte, +) (*ssh.ClientConfig, error) { + + parsedPrivateKey, err := ssh.ParsePrivateKey(privateKeyBytes) + + if err != nil { + return nil, err + } + + config := ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(parsedPrivateKey), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: connTimeout, + } + + return &config, nil +} + +// Handle local TCP server connections and tunnel data to the remote server +func (p PortForwarder) forwardLocalToRemoteConn( + localConn net.Conn, + remoteConn net.Conn, +) error { + + defer func() { + localConn.Close() + remoteConn.Close() + }() + + remoteConnRespChan := make(chan error, 1) + localConnRespChan := make(chan error, 1) + + // Forward remote -> local + go func() { + _, err := io.Copy(localConn, remoteConn) + remoteConnRespChan <- err + }() + + // Forward local -> remote + go func() { + _, err := io.Copy(remoteConn, localConn) + localConnRespChan <- err + }() + + select { + case remoteConnErr := <-remoteConnRespChan: + return remoteConnErr + case localConnErr := <-localConnRespChan: + return localConnErr + } +} diff --git a/internal/ssh/testdata/empty_known_hosts b/internal/ssh/testdata/empty_known_hosts new file mode 100644 index 0000000..e69de29 diff --git a/internal/ssh/testdata/empty_ssh_config b/internal/ssh/testdata/empty_ssh_config new file mode 100644 index 0000000..e69de29 diff --git a/internal/ssh/testdata/non_empty_known_hosts b/internal/ssh/testdata/non_empty_known_hosts new file mode 100644 index 0000000..1fb2b76 --- /dev/null +++ b/internal/ssh/testdata/non_empty_known_hosts @@ -0,0 +1,8 @@ +github.com,140.82.118.3 ssh-rsa fingerprint +34.229.126.51 ssh-rsa fingerprint + +github.com ecdsa-sha2-nistp256 fingerprint +github.com ssh-ed25519 fingerprint + +34.229.126.51 ecdsa-sha2-nistp256 fingerprint +34.229.126.51 ssh-ed25519 fingerprint diff --git a/internal/ssh/testdata/non_empty_ssh_config b/internal/ssh/testdata/non_empty_ssh_config new file mode 100644 index 0000000..4dfb8ed --- /dev/null +++ b/internal/ssh/testdata/non_empty_ssh_config @@ -0,0 +1,12 @@ +Host * + AddKeysToAgent yes + UseKeychain yes + IdentityFile ~/.ssh/id_rsa + IdentitiesOnly yes + ServerAliveInterval 240 + +Host 34.128.204.12 + HostName hostname + IdentityFile identityFile + User user + ForwardAgent yes \ No newline at end of file diff --git a/internal/stepper/step.go b/internal/stepper/step.go new file mode 100644 index 0000000..78b6e64 --- /dev/null +++ b/internal/stepper/step.go @@ -0,0 +1,20 @@ +package stepper + +import ( + "fmt" + + "github.com/briandowns/spinner" +) + +type Step struct { + spin *spinner.Spinner + removeAfterDone bool +} + +func (s *Step) Done() { + s.spin.Stop() + + if !s.removeAfterDone { + fmt.Println(s.spin.Prefix + "... done") + } +} diff --git a/internal/stepper/stepper.go b/internal/stepper/stepper.go new file mode 100644 index 0000000..7caef12 --- /dev/null +++ b/internal/stepper/stepper.go @@ -0,0 +1,97 @@ +package stepper + +import ( + "fmt" + "time" + + "github.com/briandowns/spinner" + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/recode/stepper" +) + +var currentStep *Step + +type Stepper struct{} + +func NewStepper() Stepper { + return Stepper{} +} + +func (s Stepper) startStep( + step string, + removeAfterDone bool, + noNewLineAtStart bool, +) stepper.Step { + + if currentStep == nil && !noNewLineAtStart { + fmt.Println("") + } + + if currentStep != nil { + currentStep.Done() + currentStep = nil + } + + bold := constants.Bold + + spin := spinner.New(spinner.CharSets[26], 400*time.Millisecond) + spin.Prefix = bold(step) + spin.Start() + + currentStep = &Step{ + spin: spin, + removeAfterDone: removeAfterDone, + } + + return currentStep +} + +func (s Stepper) StartStep( + step string, +) stepper.Step { + + removeAfterDone := false + noNewLineAtStart := false + + return s.startStep( + step, + removeAfterDone, + noNewLineAtStart, + ) +} + +func (s Stepper) StartTemporaryStep( + step string, +) stepper.Step { + + removeAfterDone := true + noNewLineAtStart := false + + return s.startStep( + step, + removeAfterDone, + noNewLineAtStart, + ) +} + +func (s Stepper) StartTemporaryStepWithoutNewLine( + step string, +) stepper.Step { + + removeAfterDone := true + noNewLineAtStart := true + + return s.startStep( + step, + removeAfterDone, + noNewLineAtStart, + ) +} + +func (s Stepper) StopCurrentStep() { + + if currentStep != nil { + currentStep.Done() + currentStep = nil + } +} diff --git a/internal/system/browser.go b/internal/system/browser.go new file mode 100644 index 0000000..a2ea80b --- /dev/null +++ b/internal/system/browser.go @@ -0,0 +1,15 @@ +package system + +import ( + "github.com/pkg/browser" +) + +type Browser struct{} + +func NewBrowser() Browser { + return Browser{} +} + +func (Browser) OpenURL(url string) error { + return browser.OpenURL(url) +} diff --git a/internal/system/cli.go b/internal/system/cli.go new file mode 100644 index 0000000..fad105a --- /dev/null +++ b/internal/system/cli.go @@ -0,0 +1,40 @@ +package system + +import ( + "bufio" + "io" + "strings" + + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/interfaces" +) + +func AskForConfirmation( + logger interfaces.Logger, + stdin io.Reader, + question string, +) (bool, error) { + + stdinReader := bufio.NewReader(stdin) + + logger.Log(constants.Bold(constants.Yellow("Warning!") + " " + question)) + + logger.Log("\nOnly \"yes\" will be accepted to confirm. (You could use \"--force\" next time).\n") + logger.LogNoNewline(constants.Bold("Confirm? ")) + + response, err := stdinReader.ReadString('\n') + + if err != nil { + return false, err + } + + sanitizedResponse := strings.TrimSpace(response) + + if sanitizedResponse == "yes" { + return true, nil + } + + logger.Log("") + + return false, nil +} diff --git a/internal/system/displayer.go b/internal/system/displayer.go new file mode 100644 index 0000000..0252a03 --- /dev/null +++ b/internal/system/displayer.go @@ -0,0 +1,16 @@ +package system + +import ( + "fmt" + "io" +) + +type Displayer struct{} + +func NewDisplayer() Displayer { + return Displayer{} +} + +func (Displayer) Display(w io.Writer, format string, args ...interface{}) { + fmt.Fprintf(w, format, args...) +} diff --git a/internal/system/env_vars.go b/internal/system/env_vars.go new file mode 100644 index 0000000..8a4ca90 --- /dev/null +++ b/internal/system/env_vars.go @@ -0,0 +1,13 @@ +package system + +import "os" + +type EnvVars struct{} + +func NewEnvVars() EnvVars { + return EnvVars{} +} + +func (EnvVars) Get(key string) string { + return os.Getenv(key) +} diff --git a/internal/system/logger.go b/internal/system/logger.go new file mode 100644 index 0000000..de00a64 --- /dev/null +++ b/internal/system/logger.go @@ -0,0 +1,39 @@ +package system + +import ( + "fmt" + "os" + + "github.com/recode-sh/cli/internal/constants" +) + +type Logger struct{} + +func NewLogger() Logger { + return Logger{} +} + +func (Logger) Info(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, constants.Cyan(format)+"\n", v...) +} + +func (Logger) Warning(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, constants.Yellow(format)+"\n", v...) +} + +func (Logger) Error(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, constants.Red(format)+"\n", v...) +} + +func (Logger) Log(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", v...) +} + +func (Logger) LogNoNewline(format string, v ...interface{}) { + fmt.Fprintf(os.Stderr, format, v...) +} + +func (l Logger) Write(p []byte) (n int, err error) { + l.Log(string(p)) + return len(p), nil +} diff --git a/internal/system/new_line.go b/internal/system/new_line.go new file mode 100644 index 0000000..c46c44f --- /dev/null +++ b/internal/system/new_line.go @@ -0,0 +1,11 @@ +package system + +import "runtime" + +var NewLineChar = "\n" + +func init() { + if runtime.GOOS == "windows" { + NewLineChar = "\r\n" + } +} diff --git a/internal/system/paths.go b/internal/system/paths.go new file mode 100644 index 0000000..1f8d641 --- /dev/null +++ b/internal/system/paths.go @@ -0,0 +1,64 @@ +package system + +import ( + "os" + "path/filepath" +) + +func PathExists(path string) bool { + _, err := os.Stat(path) + + if err == nil || !os.IsNotExist(err) { + return true + } + + return false +} + +func UserHomeDir() string { + // Ignore errors since we only care about Windows and *nix. + homedir, _ := os.UserHomeDir() + return homedir +} + +// UserConfigDir returns the path where +// the user config files should be stored +// following XDG Base Directory Specification. +// Ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +func UserConfigDir() string { + baseConfigDir := os.Getenv("XDG_CONFIG_HOME") + + if len(baseConfigDir) == 0 { + baseConfigDir = filepath.Join(UserHomeDir(), ".config") + } + + return filepath.Join(baseConfigDir, "recode") +} + +func UserConfigFilePath() string { + return filepath.Join(UserConfigDir(), "recode.yml") +} + +func DefaultSSHDir() string { + return filepath.Join(UserHomeDir(), ".ssh") +} + +func DefaultSSHDirExists() bool { + return PathExists(DefaultSSHDir()) +} + +func DefaultSSHConfigFilePath() string { + return filepath.Join(DefaultSSHDir(), "config") +} + +func DefaultSSHConfigFileExists() bool { + return PathExists(DefaultSSHConfigFilePath()) +} + +func DefaultSSHKnownHostsFilePath() string { + return filepath.Join(DefaultSSHDir(), "known_hosts") +} + +func DefaultSSHKnownHostsFileExists() bool { + return PathExists(DefaultSSHKnownHostsFilePath()) +} diff --git a/internal/system/paths_test.go b/internal/system/paths_test.go new file mode 100644 index 0000000..335db7a --- /dev/null +++ b/internal/system/paths_test.go @@ -0,0 +1,79 @@ +package system_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/recode-sh/cli/internal/system" +) + +func TestPathExistsWithExistingPath(t *testing.T) { + existingPath := "./testdata" + pathExists := system.PathExists(existingPath) + + if !pathExists { + t.Fatalf("expected 'true', got 'false'") + } +} + +func TestPathExistsWithNonExistingPath(t *testing.T) { + nonExistingPath := "./path-that-doesnt-exist" + pathExists := system.PathExists(nonExistingPath) + + if pathExists { + t.Fatalf("expected 'false', got 'true'") + } +} + +func TestUserHomeDir(t *testing.T) { + expectedHomeDir, err := os.UserHomeDir() + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + if system.UserHomeDir() != expectedHomeDir { + t.Fatalf( + "expected user home directory to equal '%s', got '%s'", + expectedHomeDir, + system.UserHomeDir(), + ) + } +} + +func TestDefaultSSHDir(t *testing.T) { + homeDir, err := os.UserHomeDir() + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + expectedSSHDir := filepath.Join(homeDir, ".ssh") + + if system.DefaultSSHDir() != expectedSSHDir { + t.Fatalf( + "expected default SSH directory to equal '%s', got '%s'", + expectedSSHDir, + system.DefaultSSHDir(), + ) + } +} + +func TestDefaultSSHConfigFilePath(t *testing.T) { + homeDir, err := os.UserHomeDir() + + if err != nil { + t.Fatalf("expected no error, got '%+v'", err) + } + + expectedSSHConfigFilePath := filepath.Join(homeDir, ".ssh/config") + + if system.DefaultSSHConfigFilePath() != expectedSSHConfigFilePath { + t.Fatalf( + "expected default SSH config file path to equal '%s', got '%s'", + expectedSSHConfigFilePath, + system.DefaultSSHConfigFilePath(), + ) + } +} diff --git a/internal/system/sleeper.go b/internal/system/sleeper.go new file mode 100644 index 0000000..354f998 --- /dev/null +++ b/internal/system/sleeper.go @@ -0,0 +1,15 @@ +package system + +import ( + "time" +) + +type Sleeper struct{} + +func NewSleeper() Sleeper { + return Sleeper{} +} + +func (Sleeper) Sleep(d time.Duration) { + time.Sleep(d) +} diff --git a/internal/views/base.go b/internal/views/base.go new file mode 100644 index 0000000..e5530fb --- /dev/null +++ b/internal/views/base.go @@ -0,0 +1,104 @@ +package views + +import ( + "io" + "os" + + "github.com/recode-sh/cli/internal/constants" + "github.com/recode-sh/cli/internal/presenters" +) + +//go:generate mockgen -destination=../mocks/views_displayer.go -package=mocks github.com/recode-sh/cli/internal/views Displayer +type Displayer interface { + Display(w io.Writer, format string, args ...interface{}) +} + +type BaseView struct { + Displayer Displayer +} + +func NewBaseView(displayer Displayer) BaseView { + return BaseView{ + Displayer: displayer, + } +} + +func (b BaseView) showErrorView( + err *presenters.ViewableError, + startWithNewLine bool, +) { + + bold := constants.Bold + red := constants.Red + + if startWithNewLine { + b.Displayer.Display( + os.Stdout, + "\n", + ) + } + + b.Displayer.Display( + os.Stdout, + "%s %s\n\n%s\n\n", + bold(red("Error!")), + bold(err.Title), + err.Message, + ) +} + +func (b BaseView) ShowErrorView(err *presenters.ViewableError) { + b.showErrorView(err, false) +} + +func (b BaseView) ShowErrorViewWithStartingNewLine(err *presenters.ViewableError) { + b.showErrorView(err, true) +} + +func (b BaseView) ShowWarningView(warningText, subtext string) { + bold := constants.Bold + yellow := constants.Yellow + + if len(subtext) > 0 { + b.Displayer.Display( + os.Stdout, + "%s %s\n\n%s\n\n", + bold(yellow("Warning!")), + bold(warningText), + subtext, + ) + + return + } + + b.Displayer.Display( + os.Stdout, + "%s %s\n\n", + bold(yellow("Warning!")), + bold(warningText), + ) +} + +func (b BaseView) ShowSuccessView(successText, subtext string) { + bold := constants.Bold + green := constants.Green + + if len(subtext) > 0 { + b.Displayer.Display( + os.Stdout, + "%s %s\n\n%s\n\n", + bold(green("Success!")), + bold(successText), + subtext, + ) + + return + } + + b.Displayer.Display( + os.Stdout, + "%s %s\n\n", + bold(green("Success!")), + bold(successText), + ) +} diff --git a/internal/views/login.go b/internal/views/login.go new file mode 100644 index 0000000..ceaec89 --- /dev/null +++ b/internal/views/login.go @@ -0,0 +1,22 @@ +package views + +import "github.com/recode-sh/cli/internal/presenters" + +type LoginView struct { + BaseView +} + +func NewLoginView(baseView BaseView) LoginView { + return LoginView{ + BaseView: baseView, + } +} + +func (l LoginView) View(data presenters.LoginViewData) { + if data.Error == nil { + l.ShowSuccessView(data.Content.Message, "") + return + } + + l.ShowErrorView(data.Error) +} diff --git a/internal/views/remove.go b/internal/views/remove.go new file mode 100644 index 0000000..de66f60 --- /dev/null +++ b/internal/views/remove.go @@ -0,0 +1,22 @@ +package views + +import "github.com/recode-sh/cli/internal/presenters" + +type RemoveView struct { + BaseView +} + +func NewRemoveView(baseView BaseView) RemoveView { + return RemoveView{ + BaseView: baseView, + } +} + +func (r RemoveView) View(data presenters.RemoveViewData) { + if data.Error == nil { + r.ShowSuccessView(data.Content.Message, "") + return + } + + r.ShowErrorView(data.Error) +} diff --git a/internal/views/start.go b/internal/views/start.go new file mode 100644 index 0000000..da0acd0 --- /dev/null +++ b/internal/views/start.go @@ -0,0 +1,33 @@ +package views + +import "github.com/recode-sh/cli/internal/presenters" + +type StartView struct { + BaseView +} + +func NewStartView(baseView BaseView) StartView { + return StartView{ + BaseView: baseView, + } +} + +func (s StartView) View(data presenters.StartViewData) { + if data.Error == nil { + if data.Content.ShowAsWarning { + s.ShowWarningView( + data.Content.Message, + data.Content.Subtext, + ) + return + } + + s.ShowSuccessView( + data.Content.Message, + data.Content.Subtext, + ) + return + } + + s.ShowErrorView(data.Error) +} diff --git a/internal/views/stop.go b/internal/views/stop.go new file mode 100644 index 0000000..2491f87 --- /dev/null +++ b/internal/views/stop.go @@ -0,0 +1,27 @@ +package views + +import "github.com/recode-sh/cli/internal/presenters" + +type StopView struct { + BaseView +} + +func NewStopView(baseView BaseView) StopView { + return StopView{ + BaseView: baseView, + } +} + +func (s StopView) View(data presenters.StopViewData) { + if data.Error == nil { + if data.Content.ShowAsWarning { + s.ShowWarningView(data.Content.Message, "") + return + } + + s.ShowSuccessView(data.Content.Message, "") + return + } + + s.ShowErrorView(data.Error) +} diff --git a/internal/views/uninstall.go b/internal/views/uninstall.go new file mode 100644 index 0000000..498f322 --- /dev/null +++ b/internal/views/uninstall.go @@ -0,0 +1,33 @@ +package views + +import "github.com/recode-sh/cli/internal/presenters" + +type UninstallView struct { + BaseView +} + +func NewUninstallView(baseView BaseView) UninstallView { + return UninstallView{ + BaseView: baseView, + } +} + +func (u UninstallView) View(data presenters.UninstallViewData) { + if data.Error == nil { + if data.Content.ShowAsWarning { + u.ShowWarningView( + data.Content.Message, + data.Content.Subtext, + ) + return + } + + u.ShowSuccessView( + data.Content.Message, + data.Content.Subtext, + ) + return + } + + u.ShowErrorView(data.Error) +} diff --git a/internal/vscode/cli.go b/internal/vscode/cli.go new file mode 100644 index 0000000..de7561b --- /dev/null +++ b/internal/vscode/cli.go @@ -0,0 +1,169 @@ +package vscode + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/recode-sh/cli/internal/system" +) + +type ErrCLINotFound struct { + VisitedPaths []string +} + +func (ErrCLINotFound) Error() string { + return "ErrCLINotFound" +} + +type CLI struct{} + +func (c CLI) Exec(arg ...string) (string, error) { + CLIPath, err := c.LookupPath(runtime.GOOS) + + if err != nil { + return "", err + } + + cmdOutput, err := exec.Command(CLIPath, arg...).CombinedOutput() + + return string(cmdOutput), err +} + +func (c CLI) LookupPath(operatingSystem string) (string, error) { + // First, we look for the 'code-insiders' command + insidersCLIPath, err := exec.LookPath("code-insiders") + + if err == nil { // 'code-insiders' command exists + return insidersCLIPath, nil + } + + // If the 'code-insiders' command was not found, we look for the 'code' one + CLIPath, err := exec.LookPath("code") + + if err == nil { // 'code' command exists + return CLIPath, nil + } + + // Finally, we fallback to default paths + possibleCLIPaths := []string{} + + if operatingSystem == "darwin" { // macOS + possibleCLIPaths = c.macOSPossibleCLIPaths() + } + + if operatingSystem == "windows" { + possibleCLIPaths = c.windowsPossibleCLIPaths() + } + + if operatingSystem == "linux" { + possibleCLIPaths = c.linuxPossibleCLIPaths() + } + + for _, possibleCLIPath := range possibleCLIPaths { + if system.PathExists(possibleCLIPath) { + return possibleCLIPath, nil + } + } + + return "", ErrCLINotFound{ + VisitedPaths: possibleCLIPaths, + } +} + +func (c CLI) macOSPossibleCLIPaths() []string { + rootApplicationsDir := fmt.Sprintf("%cApplications", os.PathSeparator) // /Applications + + // Order matter here. + // We want the insiders version to be matched first. + possiblePaths := []string{} + + possiblePaths = append(possiblePaths, filepath.Join( + rootApplicationsDir, + "Visual Studio Code - Insiders.app", + "Contents", + "Resources", + "app", + "bin", + "code-insiders", + )) + + possiblePaths = append(possiblePaths, filepath.Join( + rootApplicationsDir, + "Visual Studio Code.app", + "Contents", + "Resources", + "app", + "bin", + "code", + )) + + return possiblePaths +} + +func (c CLI) windowsPossibleCLIPaths() []string { + programFilesPath := os.Getenv("ProgramFiles") + + // Order matter here. + // We want the insiders version to be matched first. + + // -- Insiders VSCode versions + + possiblePaths := []string{} + + possiblePaths = append(possiblePaths, filepath.Join( + system.UserHomeDir(), + "AppData", + "Local", + "Programs", + "Microsoft VS Code Insiders", + "bin", + "code-insiders.cmd", + )) + + possiblePaths = append(possiblePaths, filepath.Join( + programFilesPath, + "Microsoft VS Code Insiders", + "bin", + "code-insiders.cmd", + )) + + // -- Regular VSCode versions + + possiblePaths = append(possiblePaths, filepath.Join( + system.UserHomeDir(), + "AppData", + "Local", + "Programs", + "Microsoft VS Code", + "bin", + "code.cmd", + )) + + possiblePaths = append(possiblePaths, filepath.Join( + programFilesPath, + "Microsoft VS Code", + "bin", + "code.cmd", + )) + + return possiblePaths +} + +func (c CLI) linuxPossibleCLIPaths() []string { + // Order matter here. + // We want the insiders version to be matched first. + possiblePaths := []string{ + "/usr/bin/code-insiders", + "/snap/bin/code-insiders", + "/usr/share/code/bin/code-insiders", + + "/usr/bin/code", + "/snap/bin/code", + "/usr/share/code/bin/code", + } + + return possiblePaths +} diff --git a/internal/vscode/extensions.go b/internal/vscode/extensions.go new file mode 100644 index 0000000..42a33cb --- /dev/null +++ b/internal/vscode/extensions.go @@ -0,0 +1,12 @@ +package vscode + +type Extensions struct{} + +func NewExtensions() Extensions { + return Extensions{} +} + +func (e Extensions) Install(extensionName string) (string, error) { + c := CLI{} + return c.Exec("--install-extension", extensionName, "--force") +} diff --git a/internal/vscode/process.go b/internal/vscode/process.go new file mode 100644 index 0000000..5a6fff5 --- /dev/null +++ b/internal/vscode/process.go @@ -0,0 +1,21 @@ +package vscode + +type Process struct{} + +func NewProcess() Process { + return Process{} +} + +func (p Process) OpenOnRemote(hostKey, pathToOpen string) (string, error) { + c := CLI{} + return c.Exec( + "--new-window", + "--skip-release-notes", + "--skip-welcome", + "--skip-add-to-recently-opened", + "--disable-workspace-trust", + "--remote", + "ssh-remote+"+hostKey, + pathToOpen, + ) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..97046c8 --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +/* +Copyright © 2022 Jeremy Levy jje.levy@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package main + +import "github.com/recode-sh/cli/internal/cmd" + +func main() { + cmd.Execute() +}