├── .envrc ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── actionlint.yml │ └── go-tests.yml ├── .gitignore ├── .gitmodules ├── .go-version ├── LICENSE ├── Makefile ├── README.md ├── component ├── component.go ├── config_source.go ├── configure.go ├── configure_test.go ├── deployment.go ├── doc.go ├── exec.go ├── id.go ├── logs.go ├── logs_plugin.go ├── mocks │ ├── access_info.go │ ├── artifact.go │ ├── authenticator.go │ ├── builder.go │ ├── builder_odr.go │ ├── config_platform.go │ ├── config_sourcer.go │ ├── configurable.go │ ├── configurable_notify.go │ ├── deployment.go │ ├── deployment_with_url.go │ ├── destroyer.go │ ├── documented.go │ ├── exec_platform.go │ ├── execer.go │ ├── generation.go │ ├── lines_chunk_writer.go │ ├── log_platform.go │ ├── log_viewer.go │ ├── logs_platform.go │ ├── mocks.go │ ├── mocks_test.go │ ├── out_parameter.go │ ├── platform.go │ ├── platform_releaser.go │ ├── proto_marshaler.go │ ├── registry.go │ ├── registry_access.go │ ├── release.go │ ├── release_manager.go │ ├── running_task.go │ ├── status.go │ ├── task_launcher.go │ ├── template.go │ └── workspace_destroyer.go ├── proto.go ├── task.go └── type_string.go ├── datadir ├── app.go ├── component.go ├── dir.go ├── dir_test.go ├── doc.go ├── project.go └── testing.go ├── docs ├── docs.go ├── docs_test.go ├── func.go ├── func_test.go └── utils.go ├── flake.lock ├── flake.nix ├── framework ├── doc.go └── resource │ ├── doc.go │ ├── manager.go │ ├── manager_test.go │ ├── resource.go │ └── resource_test.go ├── go.mod ├── go.sum ├── internal-shared ├── README.md ├── pluginclient │ └── client.go └── protomappers │ ├── mappers.go │ └── mappers_test.go ├── internal ├── funcspec │ ├── any_conv.go │ ├── args.go │ ├── func.go │ ├── func_test.go │ ├── spec.go │ └── spec_test.go ├── pkg │ ├── conpty │ │ ├── conpty.go │ │ └── syscall.go │ ├── pty │ │ ├── pty.go │ │ ├── pty_other.go │ │ └── pty_windows.go │ └── spinner │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── Makefile │ │ ├── README.md │ │ ├── _example │ │ └── main.go │ │ ├── character_sets.go │ │ └── spinner.go ├── plugin │ ├── authenticator.go │ ├── base.go │ ├── builder.go │ ├── builder_mix.go │ ├── builder_test.go │ ├── config_sourcer.go │ ├── config_sourcer_test.go │ ├── configure.go │ ├── configure_test.go │ ├── destroyer.go │ ├── destroyer_workspace.go │ ├── dynamic_call.go │ ├── error.go │ ├── exec │ │ └── exec.go │ ├── execer.go │ ├── generation.go │ ├── logs.go │ ├── logs │ │ └── logs.go │ ├── mapper.go │ ├── mapper_test.go │ ├── platform.go │ ├── platform_mix.go │ ├── platform_test.go │ ├── plugin.go │ ├── plugin_test.go │ ├── registry.go │ ├── registry_mix.go │ ├── registry_test.go │ ├── releaser.go │ ├── releaser_mix.go │ ├── releaser_test.go │ ├── status.go │ ├── task.go │ ├── task_mix.go │ ├── task_test.go │ ├── template.go │ ├── template_test.go │ └── terminal │ │ └── ui.go ├── pluginargs │ └── pluginargs.go ├── plugincomponent │ ├── access.go │ ├── artifact.go │ ├── deployment.go │ ├── doc.go │ ├── release.go │ └── task_info.go ├── stdio │ ├── stdio.go │ └── stdio_windows.go └── testproto │ ├── testproto.go │ ├── testproto.pb.go │ └── testproto.proto ├── main.go ├── proto ├── gen │ ├── plugin.pb.go │ ├── plugin_grpc.pb.go │ └── unused.pb.go ├── plugin.proto └── unused.proto ├── shell.nix ├── terminal ├── basic.go ├── display.go ├── doc.go ├── glint.go ├── glint_status.go ├── glint_step_group.go ├── glint_term.go ├── input.go ├── noninteractive.go ├── status.go ├── step.go ├── table.go ├── ui.go └── ui_test.go ├── tools.Dockerfile └── tools └── tools.go /.envrc: -------------------------------------------------------------------------------- 1 | # If we are a computer with nix-shell available, then use that to setup 2 | # the build environment with exactly what we need. 3 | if has nix-shell; then 4 | use nix 5 | fi 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/waypoint 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Waypoint Plugin SDK 2 | 3 | >**Note:** We take Waypoint's security and our users' trust very seriously. 4 | >If you believe you have found a security issue in Waypoint, please responsibly 5 | >disclose by contacting us at security@hashicorp.com. 6 | 7 | **First:** if you're unsure or afraid of _anything_, just ask or submit the 8 | issue or pull request anyways. You won't be yelled at for giving your best 9 | effort. The worst that can happen is that you'll be politely asked to change 10 | something. We appreciate any sort of contributions, and don't want a wall of 11 | rules to get in the way of that. 12 | 13 | That said, if you want to ensure that a pull request is likely to be merged, 14 | talk to us! A great way to do this is in issues themselves. When you want to 15 | work on an issue, comment on it first and tell us the approach you want to take. 16 | 17 | ## Getting Started 18 | 19 | ### Some Ways to Contribute 20 | 21 | * Report potential bugs. 22 | * Suggest product enhancements. 23 | * Increase our test coverage. 24 | * Fix a [bug](https://github.com/hashicorp/waypoint-plugin-sdk/labels/bug). 25 | * Implement a requested [enhancement](https://github.com/hashicorp/waypoint-plugin-sdk/labels/enhancement). 26 | * Improve our guides and documentation. 27 | 28 | ### Reporting an Issue: 29 | 30 | >Note: Issues on GitHub for Waypoint are intended to be related to bugs or feature requests. 31 | >Questions should be directed to other community resources such as the [forum](https://discuss.hashicorp.com/) 32 | 33 | * Make sure you test against the latest released version. It is possible we 34 | already fixed the bug you're experiencing. However, if you are on an older 35 | version of Waypoint and feel the issue is critical, do let us know. 36 | 37 | * Check existing issues (both open and closed) to make sure it has not been 38 | reported previously. 39 | 40 | * Provide a reproducible test case. If a contributor can't reproduce an issue, 41 | then it dramatically lowers the chances it'll get fixed. If we can't reproduce 42 | an issue long enough, we are usually forced to close the issue. 43 | 44 | * As part of the test case, please include any Waypoint configurations 45 | (`waypoint.hcl`), build configs such as Dockerfiles, etc. Log output with 46 | log level set with verbose flags (at least `-vv`) is helpful too. 47 | 48 | * Aim to respond promptly to any questions made by the Waypoint team on your 49 | issue. Stale issues will be closed. 50 | 51 | ### Issue Lifecycle 52 | 53 | 1. The issue is reported. 54 | 55 | 2. The issue is verified and categorized by a Waypoint maintainer. 56 | Categorization is done via tags. For example, bugs are tagged as "bug". 57 | 58 | 3. Unless it is critical, the issue is left for a period of time (sometimes many 59 | weeks or months), giving outside contributors a chance to address the issue 60 | and our internal teams time to plan for inclusion in a release. 61 | 62 | 4. The issue is addressed in a pull request or commit. The issue will be 63 | referenced in the commit message so that the code that fixes it is clearly 64 | linked. 65 | 66 | 5. The issue is closed. 67 | 68 | ## Building Waypoint Plugin SDK 69 | 70 | TODO 71 | 72 | ## Making Changes to Waypoint Plugin SDK 73 | 74 | ## Making Changes to Waypoint 75 | 76 | Run `make tools` to install the list of tools in ./tools/tools.go. 77 | >Note: If notice you have a large set of diffs due to upgrading the version of 78 | >a tool, it is best to separate out the upgrade into its own PR. 79 | 80 | The first step to making changes is to fork Waypoint. Afterwards, the easiest way 81 | to work on the fork is to set it as a remote of the Waypoint project: 82 | 83 | 1. Navigate to `$GOPATH/src/github.com/hashicorp/waypoint-plugin-sdk` 84 | 2. Rename the existing remote's name: `git remote rename origin upstream`. 85 | 3. Add your fork as a remote by running 86 | `git remote add origin `. For example: 87 | `git remote add origin https://github.com/myusername/waypoint-plugin-sdk`. 88 | 4. Checkout a feature branch: `git checkout -t -b new-feature` 89 | 5. Make changes 90 | 6. Push changes to the fork when ready to submit PR: 91 | `git push -u origin new-feature` 92 | 93 | By following these steps you can push to your fork to create a PR, but the code on disk still 94 | lives in the spot where the go cli tools are expecting to find it. 95 | 96 | >Note: If you make any changes to the code, run `make format` to automatically format the code according to Go standards. 97 | 98 | ## Testing 99 | 100 | Before submitting changes, run **all** tests locally by typing `make test`. 101 | The test suite may fail if over-parallelized, so if you are seeing stochastic 102 | failures try `GOTEST_FLAGS="-p 2 -parallel 2" make test`. 103 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know about a bug! 4 | title: '' 5 | labels: new 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | **Describe the bug** 21 | A clear and concise description of what the bug is. 22 | 23 | **Steps to Reproduce** 24 | Steps to reproduce the behavior. 25 | 26 | Please include any `waypoint.hcl` files if applicable, as well as a 27 | [GitHub Gist](https://gist.github.com/) of any relevant logs or steps to 28 | reproduce the bug. Running `waypoint` commands with `-v` up to `-vvv` will 29 | include any additional debugging info in the log. 30 | 31 | Addtionally, if you are working on a custom Waypoint plugin, a link to a 32 | repository with your code will help us triage your issue. 33 | 34 | **Expected behavior** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Waypoint Platform Versions** 38 | Additional version and platform information to help triage the issue if 39 | applicable: 40 | 41 | * Waypoint CLI Version: 42 | * Waypoint Server Platform and Version: (like `docker`, `nomad`, `kubernetes`) 43 | * Waypoint Plugin: (like `aws/ecs`, `pack`, `azure`) 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask a question 3 | url: https://discuss.hashicorp.com/c/waypoint 4 | about: For increased visibility, please post questions on the discussion forum. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest something! 4 | title: '' 5 | labels: new 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Explain any additional use-cases** 20 | If there are any use-cases that would help us understand the use/need/value please share them as they can help us decide on acceptance and prioritization. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # If the repository is public, be sure to change to GitHub hosted runners 2 | name: Lint GitHub Actions Workflows 3 | on: 4 | push: 5 | pull_request: 6 | permissions: 7 | contents: read 8 | jobs: 9 | actionlint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 13 | - name: "Check workflow files" 14 | uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest 15 | -------------------------------------------------------------------------------- /.github/workflows/go-tests.yml: -------------------------------------------------------------------------------- 1 | name: go-tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | golangci: 9 | name: golangci-lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # pin@v3.3.0 13 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@v3.5.0 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # pin@v3.4.0 16 | with: 17 | version: v1.51.2 18 | args: | 19 | --disable-all \ 20 | --timeout 10m \ 21 | --enable gofmt \ 22 | --enable gosimple \ 23 | --enable govet \ 24 | --verbose 25 | skip-pkg-cache: true 26 | skip-build-cache: true 27 | - uses: hashicorp/actions-slack-status@v1 28 | if: failure() && github.ref == 'refs/heads/main' 29 | with: 30 | failure-message: 'Linting failed' 31 | status: ${{job.status}} 32 | slack-webhook-url: ${{secrets.SLACK_WEBHOOK_URL}} 33 | check-vendor: 34 | name: check-vendor 35 | runs-on: ubuntu-latest 36 | needs: 37 | - golangci 38 | steps: 39 | - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 40 | - name: check-vendor 41 | run: | 42 | go mod tidy 43 | if ! git diff --exit-code; then 44 | echo "Git directory has vendor changes" 45 | exit 1 46 | fi 47 | - uses: hashicorp/actions-slack-status@v1 48 | if: failure() && github.ref == 'refs/heads/main' 49 | with: 50 | failure-message: 'Go mod tidyness check failed' 51 | status: ${{job.status}} 52 | slack-webhook-url: ${{secrets.SLACK_WEBHOOK_URL}} 53 | go-test: 54 | runs-on: ubuntu-latest 55 | env: 56 | GOTAGS: '' 57 | GOMAXPROCS: 4 58 | TEST_RESULTS_DIR: "/tmp/test-results" 59 | GOTESTSUM_RELEASE: 1.8.2 60 | steps: 61 | - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 62 | - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 63 | - name: Install gotestsum 64 | run: |- 65 | url=https://github.com/gotestyourself/gotestsum/releases/download 66 | curl -sSL "${url}/v${GOTESTSUM_RELEASE}/gotestsum_${GOTESTSUM_RELEASE}_linux_amd64.tar.gz" | \ 67 | sudo tar -xz --overwrite -C /usr/local/bin gotestsum 68 | - name: Mod download 69 | run: go mod download 70 | - name: go test 71 | run: |- 72 | mkdir -p "${TEST_RESULTS_DIR}" 73 | gotestsum --format=short-verbose \ 74 | --junitfile "${TEST_RESULTS_DIR}"/gotestsum-report.xml -- \ 75 | -tags="${GOTAGS}" -p 2 \ 76 | -cover -coverprofile=coverage.txt \ 77 | ./... 78 | - uses: hashicorp/actions-slack-status@v1 79 | if: failure() && github.ref == 'refs/heads/main' 80 | with: 81 | failure-message: 'Test suite failed' 82 | status: ${{job.status}} 83 | slack-webhook-url: ${{secrets.SLACK_WEBHOOK_URL}} 84 | go-generate-test: 85 | runs-on: ubuntu-latest 86 | needs: 87 | - go-test 88 | steps: 89 | - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 90 | - name: Determine Go version 91 | id: get-go-version 92 | # We use .go-version as our source of truth for current Go 93 | # version, because "goenv" can react to it automatically. 94 | run: | 95 | echo "Building with Go $(cat .go-version)" 96 | echo "go-version=$(cat .go-version)" >> "$GITHUB_OUTPUT" 97 | - name: Set up Go 98 | uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 99 | with: 100 | go-version: "${{ steps.get-go-version.outputs.go-version }}" 101 | - name: Install protoc tool 102 | run: |- 103 | url=https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-linux-x86_64.zip 104 | curl -sSL "${url}" -o protoc3173.zip 105 | unzip protoc3173.zip -d "${HOME}"/.local/protoc 106 | echo "${HOME}"/.local/protoc/bin >> "${GITHUB_PATH}" 107 | - name: Install Dependencies 108 | run: | 109 | go mod download 110 | git submodule update --init --recursive 111 | - name: Install tool dependencies 112 | run: | 113 | make --always-make tools 114 | - name: go generate 115 | run: |- 116 | make gen 117 | - uses: hashicorp/actions-slack-status@v1 118 | if: failure() && github.ref == 'refs/heads/main' 119 | with: 120 | failure-message: 'Go generate failed' 121 | status: ${{job.status}} 122 | slack-webhook-url: ${{secrets.SLACK_WEBHOOK_URL}} 123 | 124 | permissions: 125 | contents: read 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/proto/api-common-protos"] 2 | path = thirdparty/proto/api-common-protos 3 | url = https://github.com/googleapis/api-common-protos 4 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.19.4 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROTOC_VERSION="3.17.3" 2 | 3 | .PHONY: versioncheck 4 | versioncheck: # check protoc version 5 | @# Test for protoc installed 6 | @if [ $(shell which protoc | wc -l) -eq 0 ]; then \ 7 | echo "Required tool protoc not installed." \ 8 | echo "You can install the correct version from https://github.com/protocolbuffers/protobuf/releases/tag/v${PROTOC_VERSION} or consider using nix."; \ 9 | exit 1; \ 10 | fi 11 | 12 | @# Test for correct version of protoc 13 | @if [ "$(shell protoc --version | awk '{print $$2}')" != $(PROTOC_VERSION) ]; then \ 14 | echo "Incorrect version of protoc installed. $(shell protoc --version | awk '{print $2}') detected, $(PROTOC_VERSION) required."; \ 15 | echo "You can install the correct version from https://github.com/protocolbuffers/protobuf/releases/tag/v$(PROTOC_VERSION) or consider using nix."; \ 16 | exit 1; \ 17 | fi 18 | 19 | @# Test for submodule installed 20 | @test -s "thirdparty/proto/api-common-protos/.git" || { echo "git submodules not initialized, run 'git submodule update --init --recursive' and try again"; exit 1; } 21 | 22 | .PHONY: gen 23 | gen: versioncheck # generate go code 24 | go generate . 25 | 26 | .PHONY: format 27 | format: # format go code 28 | gofmt -s -w ./ 29 | 30 | .PHONY: test 31 | test: # run tests 32 | go test ./... 33 | 34 | .PHONY: tools 35 | tools: versioncheck # install dependencies and tools required to build 36 | go generate -tags tools tools/tools.go 37 | 38 | .PHONY: docker/tools 39 | docker/tools: # Creates a docker tools file for generating waypoint server protobuf files 40 | @echo "Building docker tools image" 41 | docker build -f tools.Dockerfile -t waypoint-sdk-tools:dev . 42 | 43 | .PHONY: docker/gen 44 | docker/gen: docker/tools 45 | @test -s "thirdparty/proto/api-common-protos/.git" || { echo "git submodules not initialized, run 'git submodule update --init --recursive' and try again"; exit 1; } 46 | docker run -v `pwd`:/waypoint -it docker.io/library/waypoint-sdk-tools:dev make gen 47 | -------------------------------------------------------------------------------- /component/config_source.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | // ConfigSourcer can be implemented by plugins that support sourcing 4 | // dynamic configuration for running applications. 5 | // 6 | // This plugin type runs alongside the application. The initial config loading 7 | // will block the application start so authors should attempt to source 8 | // configuration as quickly as possible. 9 | type ConfigSourcer interface { 10 | // ReadFunc returns the function for reading configuration. 11 | // 12 | // The returned function can start a background goroutine to more efficiently 13 | // watch for changes. The entrypoint will periodically call Read to check for 14 | // updates. 15 | // 16 | // If the configuration changes for any dynamic configuration variable, 17 | // the entrypoint will call Stop followed by Read, so plugins DO NOT need 18 | // to implement config diffing. Plugins may safely assume if Read is called 19 | // after a Stop that the config is new, and that subsequent calls have the 20 | // same config. 21 | // 22 | // Read is called for ALL defined configuration variables for this source. 23 | // If ANY change, Stop is called followed by Read again. Only one sourcer 24 | // is active for a set of configs. 25 | ReadFunc() interface{} 26 | 27 | // StopFunc returns a function for stopping configuration sourcing. 28 | // You can return nil if stopping is not necessary or supported for 29 | // this sourcer. 30 | // 31 | // The stop function should stop any background processes started with Read. 32 | StopFunc() interface{} 33 | } 34 | 35 | // ConfigRequest is sent to ReadFunc for ConfigSourcer to represent a 36 | // single configuration variable that was requested. The ReadFunc parameters 37 | // should have a `[]*ConfigRequest` parameter for these. 38 | type ConfigRequest struct { 39 | Name string 40 | Config map[string]string 41 | } 42 | -------------------------------------------------------------------------------- /component/configure.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/hashicorp/hcl/v2/gohcl" 6 | "github.com/hashicorp/waypoint-plugin-sdk/docs" 7 | ) 8 | 9 | // Configurable can be optionally implemented by any component to 10 | // accept user configuration. 11 | type Configurable interface { 12 | // Config should return a pointer to an allocated configuration 13 | // structure. This structure will be written to directly with the 14 | // decoded configuration. If this returns nil, then it is as if 15 | // Configurable was not implemented. 16 | Config() (interface{}, error) 17 | } 18 | 19 | // Documented can be optionally implemented by any component to 20 | // return documentation about the component. 21 | type Documented interface { 22 | // Documentation returns a completed docs.Documentation struct 23 | // describing the components configuration. 24 | Documentation() (*docs.Documentation, error) 25 | } 26 | 27 | // ConfigurableNotify is an optional interface that can be implemented 28 | // by any component to receive a notification that the configuration 29 | // was decoded. 30 | type ConfigurableNotify interface { 31 | Configurable 32 | 33 | // ConfigSet is called with the value of the configuration after 34 | // decoding is complete successfully. 35 | ConfigSet(interface{}) error 36 | } 37 | 38 | // Configure configures c with the provided configuration. 39 | // 40 | // If c does not implement Configurable AND body is non-empty, then it is 41 | // an error. If body is empty in that case, it is not an error. 42 | func Configure(c interface{}, body hcl.Body, ctx *hcl.EvalContext) hcl.Diagnostics { 43 | if c, ok := c.(Configurable); ok { 44 | // Get the configuration value 45 | v, err := c.Config() 46 | if err != nil { 47 | return hcl.Diagnostics{ 48 | &hcl.Diagnostic{ 49 | Severity: hcl.DiagError, 50 | Summary: err.Error(), 51 | Detail: "", 52 | }, 53 | } 54 | } 55 | 56 | // If the configuration structure is nil then we behave as if the 57 | // component is not configurable. 58 | if v == nil { 59 | return nil 60 | } 61 | 62 | // Decode 63 | if diag := gohcl.DecodeBody(body, ctx, v); len(diag) > 0 { 64 | return diag 65 | } 66 | 67 | // If decoding worked and we have a notification implementation, then 68 | // notify with the value. 69 | if cn, ok := c.(ConfigurableNotify); ok { 70 | if err := cn.ConfigSet(v); err != nil { 71 | return hcl.Diagnostics{ 72 | &hcl.Diagnostic{ 73 | Severity: hcl.DiagError, 74 | Summary: err.Error(), 75 | Detail: "", 76 | }, 77 | } 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // If c doesn't implement Configurable, then we parse the content with 85 | // an empty schema which will error if there are any fields since its 86 | // non-conformant to the schema. 87 | _, diag := body.Content(&hcl.BodySchema{}) 88 | return diag 89 | } 90 | 91 | // Documentation returns the documentation for the given component. 92 | // 93 | // If c does not implement Documented, nil is returned. 94 | func Documentation(c interface{}) (*docs.Documentation, error) { 95 | if d, ok := c.(Documented); ok { 96 | return d.Documentation() 97 | } 98 | 99 | // Build up options we'll use to build our documentation implicitly 100 | var opts []docs.Option 101 | 102 | // Get the configuration value if we have one. 103 | if c, ok := c.(Configurable); ok { 104 | v, err := c.Config() 105 | if err == nil && v != nil { 106 | opts = append(opts, docs.FromConfig(v)) 107 | } 108 | } 109 | 110 | // Determine if we have a function type to populate 111 | switch typ := c.(type) { 112 | case Builder: 113 | opts = append(opts, docs.FromFunc(typ.BuildFunc())) 114 | 115 | case Registry: 116 | opts = append(opts, docs.FromFunc(typ.PushFunc())) 117 | 118 | case Platform: 119 | opts = append(opts, docs.FromFunc(typ.DeployFunc())) 120 | } 121 | 122 | // Return. If we implemented nothing this will just be an empty docs value. 123 | return docs.New(opts...) 124 | } 125 | -------------------------------------------------------------------------------- /component/configure_test.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/hashicorp/hcl/v2/hclparse" 8 | "github.com/hashicorp/hcl/v2/hclsimple" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestConfigure(t *testing.T) { 13 | t.Run("valid config", func(t *testing.T) { 14 | require := require.New(t) 15 | 16 | var c impl 17 | src := `name = "foo"` 18 | f, diag := hclparse.NewParser().ParseHCL([]byte(src), "test.hcl") 19 | require.False(diag.HasErrors()) 20 | 21 | diag = Configure(&c, f.Body, nil) 22 | require.False(diag.HasErrors()) 23 | require.Equal(c.config.Name, "foo") 24 | }) 25 | 26 | t.Run("invalid config", func(t *testing.T) { 27 | require := require.New(t) 28 | 29 | var c impl 30 | src := `` 31 | f, diag := hclparse.NewParser().ParseHCL([]byte(src), "test.hcl") 32 | require.False(diag.HasErrors()) 33 | 34 | diag = Configure(&c, f.Body, nil) 35 | require.True(diag.HasErrors()) 36 | require.Contains(diag.Error(), "is required") 37 | }) 38 | 39 | t.Run("empty body", func(t *testing.T) { 40 | require := require.New(t) 41 | 42 | var s struct { 43 | Block struct { 44 | Label string `hcl:",label"` 45 | Body hcl.Body `hcl:",remain"` 46 | } `hcl:"block,block"` 47 | } 48 | 49 | src := `block "foo" {}` 50 | require.NoError(hclsimple.Decode("test.hcl", []byte(src), nil, &s)) 51 | 52 | var c impl 53 | diag := Configure(&c, s.Block.Body, nil) 54 | require.True(diag.HasErrors()) 55 | require.Contains(diag.Error(), "is required") 56 | }) 57 | 58 | t.Run("nil interface", func(t *testing.T) { 59 | require := require.New(t) 60 | 61 | var s struct { 62 | Block struct { 63 | Label string `hcl:",label"` 64 | Body hcl.Body `hcl:",remain"` 65 | } `hcl:"block,block"` 66 | } 67 | 68 | src := `block "foo" {}` 69 | require.NoError(hclsimple.Decode("test.hcl", []byte(src), nil, &s)) 70 | 71 | diag := Configure(nil, s.Block.Body, nil) 72 | require.False(diag.HasErrors()) 73 | }) 74 | 75 | t.Run("nil config struct", func(t *testing.T) { 76 | require := require.New(t) 77 | 78 | var s struct { 79 | Block struct { 80 | Label string `hcl:",label"` 81 | Body hcl.Body `hcl:",remain"` 82 | } `hcl:"block,block"` 83 | } 84 | 85 | src := `block "foo" {}` 86 | require.NoError(hclsimple.Decode("test.hcl", []byte(src), nil, &s)) 87 | 88 | var c implNil 89 | diag := Configure(&c, s.Block.Body, nil) 90 | require.False(diag.HasErrors()) 91 | }) 92 | } 93 | 94 | func TestConfigure_nonImpl(t *testing.T) { 95 | t.Run("empty body", func(t *testing.T) { 96 | require := require.New(t) 97 | 98 | var s struct { 99 | Block struct { 100 | Label string `hcl:",label"` 101 | Body hcl.Body `hcl:",remain"` 102 | } `hcl:"block,block"` 103 | } 104 | 105 | src := `block "foo" {}` 106 | require.NoError(hclsimple.Decode("test.hcl", []byte(src), nil, &s)) 107 | 108 | var c struct{} 109 | diag := Configure(&c, s.Block.Body, nil) 110 | require.False(diag.HasErrors()) 111 | }) 112 | 113 | t.Run("body", func(t *testing.T) { 114 | require := require.New(t) 115 | 116 | src := `name = "foo"` 117 | f, diag := hclparse.NewParser().ParseHCL([]byte(src), "test.hcl") 118 | require.False(diag.HasErrors()) 119 | 120 | var c struct{} 121 | diag = Configure(&c, f.Body, nil) 122 | require.True(diag.HasErrors()) 123 | t.Log(diag.Error()) 124 | }) 125 | } 126 | 127 | func TestConfigure_notify(t *testing.T) { 128 | t.Run("valid config", func(t *testing.T) { 129 | require := require.New(t) 130 | 131 | src := `name = "foo"` 132 | f, diag := hclparse.NewParser().ParseHCL([]byte(src), "test.hcl") 133 | require.False(diag.HasErrors()) 134 | 135 | var c implNotify 136 | diag = Configure(&c, f.Body, nil) 137 | require.False(diag.HasErrors()) 138 | require.Equal(c.config.Name, "foo") 139 | require.True(c.Notified) 140 | }) 141 | } 142 | 143 | type testConfig struct { 144 | Name string `hcl:"name,attr"` 145 | } 146 | 147 | type impl struct{ config testConfig } 148 | 149 | func (c *impl) Config() (interface{}, error) { return &c.config, nil } 150 | 151 | type implNil struct{} 152 | 153 | func (c *implNil) Config() (interface{}, error) { return nil, nil } 154 | 155 | type implNotify struct { 156 | impl 157 | Notified bool 158 | } 159 | 160 | func (c *implNotify) ConfigSet(interface{}) error { 161 | c.Notified = true 162 | return nil 163 | } 164 | 165 | var ( 166 | _ Configurable = (*implNotify)(nil) 167 | _ ConfigurableNotify = (*implNotify)(nil) 168 | ) 169 | -------------------------------------------------------------------------------- /component/deployment.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | // DeploymentConfig is the configuration for the behavior of a deployment. 4 | // Platforms should take this argument and use the value to set the appropriate 5 | // settings for the deployment 6 | type DeploymentConfig struct { 7 | Id string 8 | Sequence uint64 9 | ServerAddr string 10 | ServerTls bool 11 | ServerTlsSkipVerify bool 12 | EntrypointInviteToken string 13 | } 14 | 15 | // Env returns the environment variables that should be set for the entrypoint 16 | // binary to have the proper configuration. 17 | func (c *DeploymentConfig) Env() map[string]string { 18 | results := map[string]string{ 19 | "WAYPOINT_DEPLOYMENT_ID": c.Id, 20 | } 21 | 22 | if c.ServerAddr == "" { 23 | // If the server is disabled we set this env var. Note that having 24 | // no address given also causes it to behave the same way. 25 | results["WAYPOINT_SERVER_DISABLE"] = "1" 26 | } else { 27 | // Note the server address. 28 | results["WAYPOINT_SERVER_ADDR"] = c.ServerAddr 29 | if c.ServerTls { 30 | results["WAYPOINT_SERVER_TLS"] = "1" 31 | } 32 | if c.ServerTlsSkipVerify { 33 | results["WAYPOINT_SERVER_TLS_SKIP_VERIFY"] = "1" 34 | } 35 | 36 | // Set our token if we have one 37 | if c.EntrypointInviteToken != "" { 38 | results["WAYPOINT_CEB_INVITE_TOKEN"] = c.EntrypointInviteToken 39 | } 40 | } 41 | 42 | return results 43 | } 44 | -------------------------------------------------------------------------------- /component/doc.go: -------------------------------------------------------------------------------- 1 | // Package component exposes the component types supported and helpers around 2 | // those types. 3 | package component 4 | -------------------------------------------------------------------------------- /component/exec.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "io" 4 | 5 | // ExecSessionInfo contains the information required by the exec plugin 6 | // to setup a new exec and send the data back to a client. 7 | // A ExecSessionInfo value is passed to a plugins ExecFunc() to allow 8 | // the function to properly create the exec session. 9 | type ExecSessionInfo struct { 10 | Input io.Reader // effectively the stdin from the user (stdin) 11 | Output io.Writer // the output from the session (stdout) 12 | Error io.Writer // an error output from the session (stderr) 13 | 14 | IsTTY bool // indicates if the input/output is a terminal 15 | 16 | // If this is a TTY, this is the terminal type (ie, the value of the TERM 17 | // environment variable) 18 | Term string 19 | 20 | // If this is a TTY, this is the initial window size. 21 | InitialWindowSize WindowSize 22 | 23 | // a channel that is sent the size of the users window. A new value 24 | // is sent on start and on each detection of window change. 25 | WindowSizeUpdates <-chan WindowSize 26 | 27 | // arguments to pass to the session. Normally interpreted as the first value 28 | // being the command to run and the rest arguments to that command. 29 | Arguments []string 30 | 31 | // environment variables to set within the context of the session. 32 | // This will contain configuration variables from the server 33 | // as well as any variable derived from external systems like vault 34 | // or kubernetes. 35 | Environment []string 36 | } 37 | 38 | // WindowSize provides information about the size of the terminal 39 | // window. 40 | type WindowSize struct { 41 | Height int // the height (in lines) of the terminal 42 | Width int // the width (in lines) of the terminal 43 | } 44 | -------------------------------------------------------------------------------- /component/id.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "crypto/rand" 5 | 6 | "github.com/oklog/ulid" 7 | ) 8 | 9 | var ulidReader = ulid.Monotonic(rand.Reader, 1) 10 | 11 | // Id returns a unique Id that can be used for new values. This generates 12 | // a ulid value but the ID itself should be an internal detail. An error will 13 | // be returned if the ID could be generated. 14 | func Id() (string, error) { 15 | id, err := ulid.New(ulid.Now(), ulidReader) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | return id.String(), nil 21 | } 22 | -------------------------------------------------------------------------------- /component/logs.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // LogViewer returns batches of log lines. This is expected to be returned 8 | // by a LogPlatform implementation. 9 | type LogViewer struct { 10 | // This is the time horizon log entries must be beyond to be emitted. 11 | StartingAt time.Time 12 | 13 | // The maximum number of log entries to emit. 14 | Limit int 15 | 16 | // New LogEvents should be sent to this channel. 17 | Output chan LogEvent 18 | } 19 | 20 | // LogEvent represents a single log entry. 21 | type LogEvent struct { 22 | Partition string 23 | Timestamp time.Time 24 | Message string 25 | } 26 | -------------------------------------------------------------------------------- /component/logs_plugin.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "time" 4 | 5 | // LinesChunkWriter is used by a logs plugin to output a chunk of lines. 6 | // A concrete implementation is provided to the plugin inside the LogsSessionInfo 7 | // value. 8 | type LinesChunkWriter interface { 9 | OutputLines([]string) error 10 | } 11 | 12 | // ExecSessionInfo contains the information required by the exec plugin 13 | // to setup a new exec and send the data back to a client. 14 | // A ExecSessionInfo value is passed to a plugins ExecFunc() to allow 15 | // the function to properly create the exec session. 16 | type LogsSessionInfo struct { 17 | Output LinesChunkWriter 18 | 19 | // The time horizon to begin showing logs from. Only show logs newer 20 | // than this time stamp. 21 | StartingFrom time.Time 22 | 23 | // The maximum number of lines to output. 24 | Limit int 25 | } 26 | -------------------------------------------------------------------------------- /component/mocks/access_info.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // AccessInfo is an autogenerated mock type for the AccessInfo type 8 | type AccessInfo struct { 9 | mock.Mock 10 | } 11 | -------------------------------------------------------------------------------- /component/mocks/artifact.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Artifact is an autogenerated mock type for the Artifact type 8 | type Artifact struct { 9 | mock.Mock 10 | } 11 | 12 | // Labels provides a mock function with given fields: 13 | func (_m *Artifact) Labels() map[string]string { 14 | ret := _m.Called() 15 | 16 | var r0 map[string]string 17 | if rf, ok := ret.Get(0).(func() map[string]string); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(map[string]string) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/authenticator.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Authenticator is an autogenerated mock type for the Authenticator type 8 | type Authenticator struct { 9 | mock.Mock 10 | } 11 | 12 | // AuthFunc provides a mock function with given fields: 13 | func (_m *Authenticator) AuthFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // ValidateAuthFunc provides a mock function with given fields: 29 | func (_m *Authenticator) ValidateAuthFunc() interface{} { 30 | ret := _m.Called() 31 | 32 | var r0 interface{} 33 | if rf, ok := ret.Get(0).(func() interface{}); ok { 34 | r0 = rf() 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).(interface{}) 38 | } 39 | } 40 | 41 | return r0 42 | } 43 | -------------------------------------------------------------------------------- /component/mocks/builder.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Builder is an autogenerated mock type for the Builder type 8 | type Builder struct { 9 | mock.Mock 10 | } 11 | 12 | // BuildFunc provides a mock function with given fields: 13 | func (_m *Builder) BuildFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/builder_odr.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // BuilderODR is an autogenerated mock type for the BuilderODR type 8 | type BuilderODR struct { 9 | mock.Mock 10 | } 11 | 12 | // BuildODRFunc provides a mock function with given fields: 13 | func (_m *BuilderODR) BuildODRFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/config_platform.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ConfigPlatform is an autogenerated mock type for the ConfigPlatform type 8 | type ConfigPlatform struct { 9 | mock.Mock 10 | } 11 | 12 | // ConfigGetFunc provides a mock function with given fields: 13 | func (_m *ConfigPlatform) ConfigGetFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // ConfigSetFunc provides a mock function with given fields: 29 | func (_m *ConfigPlatform) ConfigSetFunc() interface{} { 30 | ret := _m.Called() 31 | 32 | var r0 interface{} 33 | if rf, ok := ret.Get(0).(func() interface{}); ok { 34 | r0 = rf() 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).(interface{}) 38 | } 39 | } 40 | 41 | return r0 42 | } 43 | -------------------------------------------------------------------------------- /component/mocks/config_sourcer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ConfigSourcer is an autogenerated mock type for the ConfigSourcer type 8 | type ConfigSourcer struct { 9 | mock.Mock 10 | } 11 | 12 | // ReadFunc provides a mock function with given fields: 13 | func (_m *ConfigSourcer) ReadFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // StopFunc provides a mock function with given fields: 29 | func (_m *ConfigSourcer) StopFunc() interface{} { 30 | ret := _m.Called() 31 | 32 | var r0 interface{} 33 | if rf, ok := ret.Get(0).(func() interface{}); ok { 34 | r0 = rf() 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).(interface{}) 38 | } 39 | } 40 | 41 | return r0 42 | } 43 | -------------------------------------------------------------------------------- /component/mocks/configurable.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Configurable is an autogenerated mock type for the Configurable type 8 | type Configurable struct { 9 | mock.Mock 10 | } 11 | 12 | // Config provides a mock function with given fields: 13 | func (_m *Configurable) Config() (interface{}, error) { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | var r1 error 26 | if rf, ok := ret.Get(1).(func() error); ok { 27 | r1 = rf() 28 | } else { 29 | r1 = ret.Error(1) 30 | } 31 | 32 | return r0, r1 33 | } 34 | -------------------------------------------------------------------------------- /component/mocks/configurable_notify.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ConfigurableNotify is an autogenerated mock type for the ConfigurableNotify type 8 | type ConfigurableNotify struct { 9 | mock.Mock 10 | } 11 | 12 | // Config provides a mock function with given fields: 13 | func (_m *ConfigurableNotify) Config() (interface{}, error) { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | var r1 error 26 | if rf, ok := ret.Get(1).(func() error); ok { 27 | r1 = rf() 28 | } else { 29 | r1 = ret.Error(1) 30 | } 31 | 32 | return r0, r1 33 | } 34 | 35 | // ConfigSet provides a mock function with given fields: _a0 36 | func (_m *ConfigurableNotify) ConfigSet(_a0 interface{}) error { 37 | ret := _m.Called(_a0) 38 | 39 | var r0 error 40 | if rf, ok := ret.Get(0).(func(interface{}) error); ok { 41 | r0 = rf(_a0) 42 | } else { 43 | r0 = ret.Error(0) 44 | } 45 | 46 | return r0 47 | } 48 | -------------------------------------------------------------------------------- /component/mocks/deployment.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Deployment is an autogenerated mock type for the Deployment type 8 | type Deployment struct { 9 | mock.Mock 10 | } 11 | -------------------------------------------------------------------------------- /component/mocks/deployment_with_url.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // DeploymentWithUrl is an autogenerated mock type for the DeploymentWithUrl type 8 | type DeploymentWithUrl struct { 9 | mock.Mock 10 | } 11 | 12 | // URL provides a mock function with given fields: 13 | func (_m *DeploymentWithUrl) URL() string { 14 | ret := _m.Called() 15 | 16 | var r0 string 17 | if rf, ok := ret.Get(0).(func() string); ok { 18 | r0 = rf() 19 | } else { 20 | r0 = ret.Get(0).(string) 21 | } 22 | 23 | return r0 24 | } 25 | -------------------------------------------------------------------------------- /component/mocks/destroyer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Destroyer is an autogenerated mock type for the Destroyer type 8 | type Destroyer struct { 9 | mock.Mock 10 | } 11 | 12 | // DestroyFunc provides a mock function with given fields: 13 | func (_m *Destroyer) DestroyFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/documented.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | docs "github.com/hashicorp/waypoint-plugin-sdk/docs" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // Documented is an autogenerated mock type for the Documented type 11 | type Documented struct { 12 | mock.Mock 13 | } 14 | 15 | // Documentation provides a mock function with given fields: 16 | func (_m *Documented) Documentation() (*docs.Documentation, error) { 17 | ret := _m.Called() 18 | 19 | var r0 *docs.Documentation 20 | if rf, ok := ret.Get(0).(func() *docs.Documentation); ok { 21 | r0 = rf() 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0 = ret.Get(0).(*docs.Documentation) 25 | } 26 | } 27 | 28 | var r1 error 29 | if rf, ok := ret.Get(1).(func() error); ok { 30 | r1 = rf() 31 | } else { 32 | r1 = ret.Error(1) 33 | } 34 | 35 | return r0, r1 36 | } 37 | -------------------------------------------------------------------------------- /component/mocks/exec_platform.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ExecPlatform is an autogenerated mock type for the ExecPlatform type 8 | type ExecPlatform struct { 9 | mock.Mock 10 | } 11 | 12 | // ExecFunc provides a mock function with given fields: 13 | func (_m *ExecPlatform) ExecFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/execer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Execer is an autogenerated mock type for the Execer type 8 | type Execer struct { 9 | mock.Mock 10 | } 11 | 12 | // ExecFunc provides a mock function with given fields: 13 | func (_m *Execer) ExecFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/generation.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Generation is an autogenerated mock type for the Generation type 8 | type Generation struct { 9 | mock.Mock 10 | } 11 | 12 | // GenerationFunc provides a mock function with given fields: 13 | func (_m *Generation) GenerationFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/lines_chunk_writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // LinesChunkWriter is an autogenerated mock type for the LinesChunkWriter type 8 | type LinesChunkWriter struct { 9 | mock.Mock 10 | } 11 | 12 | // OutputLines provides a mock function with given fields: _a0 13 | func (_m *LinesChunkWriter) OutputLines(_a0 []string) error { 14 | ret := _m.Called(_a0) 15 | 16 | var r0 error 17 | if rf, ok := ret.Get(0).(func([]string) error); ok { 18 | r0 = rf(_a0) 19 | } else { 20 | r0 = ret.Error(0) 21 | } 22 | 23 | return r0 24 | } 25 | -------------------------------------------------------------------------------- /component/mocks/log_platform.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // LogPlatform is an autogenerated mock type for the LogPlatform type 8 | type LogPlatform struct { 9 | mock.Mock 10 | } 11 | 12 | // LogsFunc provides a mock function with given fields: 13 | func (_m *LogPlatform) LogsFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/log_viewer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | component "github.com/hashicorp/waypoint-plugin-sdk/component" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // LogViewer is an autogenerated mock type for the LogViewer type 14 | type LogViewer struct { 15 | mock.Mock 16 | } 17 | 18 | // NextLogBatch provides a mock function with given fields: ctx 19 | func (_m *LogViewer) NextLogBatch(ctx context.Context) ([]component.LogEvent, error) { 20 | ret := _m.Called(ctx) 21 | 22 | var r0 []component.LogEvent 23 | if rf, ok := ret.Get(0).(func(context.Context) []component.LogEvent); ok { 24 | r0 = rf(ctx) 25 | } else { 26 | if ret.Get(0) != nil { 27 | r0 = ret.Get(0).([]component.LogEvent) 28 | } 29 | } 30 | 31 | var r1 error 32 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 33 | r1 = rf(ctx) 34 | } else { 35 | r1 = ret.Error(1) 36 | } 37 | 38 | return r0, r1 39 | } 40 | -------------------------------------------------------------------------------- /component/mocks/logs_platform.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // LogsPlatform is an autogenerated mock type for the LogsPlatform type 8 | type LogsPlatform struct { 9 | mock.Mock 10 | } 11 | 12 | // LogsFunc provides a mock function with given fields: 13 | func (_m *LogsPlatform) LogsFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/mocks.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/stretchr/testify/mock" 7 | 8 | "github.com/hashicorp/waypoint-plugin-sdk/component" 9 | ) 10 | 11 | // ForType returns an implementation of the given type that supports mocking. 12 | func ForType(t component.Type) interface{} { 13 | // Note that the tests in mocks_test.go verify that we support all types 14 | switch t { 15 | case component.BuilderType: 16 | return &Builder{} 17 | 18 | case component.RegistryType: 19 | return &Registry{} 20 | 21 | case component.PlatformType: 22 | return &Platform{} 23 | 24 | case component.LogPlatformType: 25 | return &LogPlatform{} 26 | 27 | case component.ReleaseManagerType: 28 | return &ReleaseManager{} 29 | 30 | case component.AuthenticatorType: 31 | return &Authenticator{} 32 | 33 | case component.ConfigSourcerType: 34 | return &ConfigSourcer{} 35 | 36 | case component.TaskLauncherType: 37 | return &TaskLauncher{} 38 | 39 | default: 40 | return nil 41 | } 42 | } 43 | 44 | // Mock returns the Mock field for the given interface. The interface value 45 | // should be one of the mocks in this package. This will panic if an incorrect 46 | // value is given, error checking is not done. 47 | func Mock(v interface{}) *mock.Mock { 48 | value := reflect.ValueOf(v) 49 | if value.Kind() == reflect.Interface { 50 | value = reflect.Indirect(value) 51 | } 52 | if value.Kind() == reflect.Ptr { 53 | value = value.Elem() 54 | } 55 | 56 | field := value.FieldByName("Mock") 57 | return field.Addr().Interface().(*mock.Mock) 58 | } 59 | -------------------------------------------------------------------------------- /component/mocks/mocks_test.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/hashicorp/waypoint-plugin-sdk/component" 9 | ) 10 | 11 | func TestForType(t *testing.T) { 12 | for typ := range component.TypeMap { 13 | t.Run(typ.String(), func(t *testing.T) { 14 | require.NotNil(t, ForType(typ)) 15 | }) 16 | } 17 | } 18 | 19 | func TestMock_typed(t *testing.T) { 20 | require := require.New(t) 21 | 22 | b := &Builder{} 23 | m := Mock(b) 24 | require.NotNil(m) 25 | require.Equal(m, &b.Mock) 26 | } 27 | 28 | func TestMock_allTypes(t *testing.T) { 29 | for typ := range component.TypeMap { 30 | t.Run(typ.String(), func(t *testing.T) { 31 | require := require.New(t) 32 | v := ForType(typ) 33 | m := Mock(v) 34 | require.NotNil(m) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /component/mocks/out_parameter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // OutParameter is an autogenerated mock type for the OutParameter type 8 | type OutParameter struct { 9 | mock.Mock 10 | } 11 | 12 | // isOutParameter provides a mock function with given fields: 13 | func (_m *OutParameter) isOutParameter() { 14 | _m.Called() 15 | } 16 | -------------------------------------------------------------------------------- /component/mocks/platform.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Platform is an autogenerated mock type for the Platform type 8 | type Platform struct { 9 | mock.Mock 10 | } 11 | 12 | // DeployFunc provides a mock function with given fields: 13 | func (_m *Platform) DeployFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/platform_releaser.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // PlatformReleaser is an autogenerated mock type for the PlatformReleaser type 8 | type PlatformReleaser struct { 9 | mock.Mock 10 | } 11 | 12 | // DefaultReleaserFunc provides a mock function with given fields: 13 | func (_m *PlatformReleaser) DefaultReleaserFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/proto_marshaler.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 8 | ) 9 | 10 | // ProtoMarshaler is an autogenerated mock type for the ProtoMarshaler type 11 | type ProtoMarshaler struct { 12 | mock.Mock 13 | } 14 | 15 | // Proto provides a mock function with given fields: 16 | func (_m *ProtoMarshaler) Proto() protoreflect.ProtoMessage { 17 | ret := _m.Called() 18 | 19 | var r0 protoreflect.ProtoMessage 20 | if rf, ok := ret.Get(0).(func() protoreflect.ProtoMessage); ok { 21 | r0 = rf() 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0 = ret.Get(0).(protoreflect.ProtoMessage) 25 | } 26 | } 27 | 28 | return r0 29 | } 30 | -------------------------------------------------------------------------------- /component/mocks/registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Registry is an autogenerated mock type for the Registry type 8 | type Registry struct { 9 | mock.Mock 10 | } 11 | 12 | // PushFunc provides a mock function with given fields: 13 | func (_m *Registry) PushFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/registry_access.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // RegistryAccess is an autogenerated mock type for the RegistryAccess type 8 | type RegistryAccess struct { 9 | mock.Mock 10 | } 11 | 12 | // AccessInfoFunc provides a mock function with given fields: 13 | func (_m *RegistryAccess) AccessInfoFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/release.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Release is an autogenerated mock type for the Release type 8 | type Release struct { 9 | mock.Mock 10 | } 11 | 12 | // URL provides a mock function with given fields: 13 | func (_m *Release) URL() string { 14 | ret := _m.Called() 15 | 16 | var r0 string 17 | if rf, ok := ret.Get(0).(func() string); ok { 18 | r0 = rf() 19 | } else { 20 | r0 = ret.Get(0).(string) 21 | } 22 | 23 | return r0 24 | } 25 | -------------------------------------------------------------------------------- /component/mocks/release_manager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ReleaseManager is an autogenerated mock type for the ReleaseManager type 8 | type ReleaseManager struct { 9 | mock.Mock 10 | } 11 | 12 | // ReleaseFunc provides a mock function with given fields: 13 | func (_m *ReleaseManager) ReleaseFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/running_task.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // RunningTask is an autogenerated mock type for the RunningTask type 8 | type RunningTask struct { 9 | mock.Mock 10 | } 11 | -------------------------------------------------------------------------------- /component/mocks/status.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Status is an autogenerated mock type for the Status type 8 | type Status struct { 9 | mock.Mock 10 | } 11 | 12 | // StatusFunc provides a mock function with given fields: 13 | func (_m *Status) StatusFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/task_launcher.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // TaskLauncher is an autogenerated mock type for the TaskLauncher type 8 | type TaskLauncher struct { 9 | mock.Mock 10 | } 11 | 12 | // StartTaskFunc provides a mock function with given fields: 13 | func (_m *TaskLauncher) StartTaskFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // StopTaskFunc provides a mock function with given fields: 29 | func (_m *TaskLauncher) StopTaskFunc() interface{} { 30 | ret := _m.Called() 31 | 32 | var r0 interface{} 33 | if rf, ok := ret.Get(0).(func() interface{}); ok { 34 | r0 = rf() 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).(interface{}) 38 | } 39 | } 40 | 41 | return r0 42 | } 43 | 44 | // WatchTaskFunc provides a mock function with given fields: 45 | func (_m *TaskLauncher) WatchTaskFunc() interface{} { 46 | ret := _m.Called() 47 | 48 | var r0 interface{} 49 | if rf, ok := ret.Get(0).(func() interface{}); ok { 50 | r0 = rf() 51 | } else { 52 | if ret.Get(0) != nil { 53 | r0 = ret.Get(0).(interface{}) 54 | } 55 | } 56 | 57 | return r0 58 | } 59 | -------------------------------------------------------------------------------- /component/mocks/template.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Template is an autogenerated mock type for the Template type 8 | type Template struct { 9 | mock.Mock 10 | } 11 | 12 | // TemplateData provides a mock function with given fields: 13 | func (_m *Template) TemplateData() map[string]interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 map[string]interface{} 17 | if rf, ok := ret.Get(0).(func() map[string]interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(map[string]interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/mocks/workspace_destroyer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.1.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // WorkspaceDestroyer is an autogenerated mock type for the WorkspaceDestroyer type 8 | type WorkspaceDestroyer struct { 9 | mock.Mock 10 | } 11 | 12 | // DestroyWorkspaceFunc provides a mock function with given fields: 13 | func (_m *WorkspaceDestroyer) DestroyWorkspaceFunc() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | -------------------------------------------------------------------------------- /component/proto.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/hashicorp/opaqueany" 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | "google.golang.org/protobuf/proto" 10 | "google.golang.org/protobuf/types/known/anypb" 11 | ) 12 | 13 | // ProtoMarshaler is the interface required by objects that must support 14 | // protobuf marshaling. This expects the object to go to a proto.Message 15 | // which is converted to a proto Any value[1]. The plugin is expected to 16 | // register a proto type that can decode this Any value. 17 | // 18 | // This enables the project to encode intermediate objects (such as artifacts) 19 | // and store them in a database. 20 | // 21 | // [1]: https://developers.google.com/protocol-buffers/docs/proto3#any 22 | type ProtoMarshaler interface { 23 | // Proto returns a proto.Message of this structure. This may also return 24 | // a proto Any value and it will not be re-wrapped with Any. 25 | Proto() proto.Message 26 | } 27 | 28 | // Proto returns the proto.Message for a given value that is either already 29 | // a proto.Message or implements ProtoMarshaler. If the value implements neither, 30 | // then this returns (nil, nil). 31 | func Proto(m interface{}) (proto.Message, error) { 32 | msg, ok := m.(proto.Message) 33 | if ok { 34 | return msg, nil 35 | } 36 | 37 | // If it isn't a message directly, we accept marshalers 38 | pm, ok := m.(ProtoMarshaler) 39 | if !ok { 40 | return nil, nil 41 | } 42 | 43 | return pm.Proto(), nil 44 | } 45 | 46 | // ProtoAny returns an *opaqueany.Any for the given ProtoMarshaler object. This 47 | // will return nil if the given message is nil. 48 | func ProtoAny(m interface{}) (*opaqueany.Any, error) { 49 | msg, err := Proto(m) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | // If the message is already an opaqueany.Any, then we're done 55 | if result, ok := msg.(*opaqueany.Any); ok { 56 | return result, nil 57 | } 58 | 59 | // If the message is an any.Any, then convert it to an opaqueany.Any 60 | if result, ok := msg.(*anypb.Any); ok { 61 | return &opaqueany.Any{ 62 | TypeUrl: result.TypeUrl, 63 | Value: result.Value, 64 | }, nil 65 | } 66 | 67 | // If we have a nil message, we don't marshal anything. 68 | if msg == nil { 69 | return nil, nil 70 | } 71 | 72 | return opaqueany.New(msg) 73 | } 74 | 75 | // ProtoAny returns []*opaqueany.Any for the given input slice by encoding 76 | // each result into a proto value. 77 | func ProtoAnySlice(m interface{}) ([]*opaqueany.Any, error) { 78 | val := reflect.ValueOf(m) 79 | result := make([]*opaqueany.Any, val.Len()) 80 | for i := 0; i < val.Len(); i++ { 81 | var err error 82 | result[i], err = ProtoAny(val.Index(i).Interface()) 83 | if err != nil { 84 | return nil, err 85 | } 86 | } 87 | 88 | return result, nil 89 | } 90 | 91 | // ProtoAnyUnmarshal attempts to unmarshal a ProtoMarshler implementation 92 | // to another type. This can be used to get more concrete data out of a 93 | // generic component. 94 | func ProtoAnyUnmarshal(m interface{}, out proto.Message) error { 95 | msg, ok := m.(proto.Message) 96 | 97 | // If it isn't a message directly, we accept marshalers 98 | if !ok { 99 | pm, ok := m.(ProtoMarshaler) 100 | if !ok { 101 | return status.Errorf(codes.FailedPrecondition, 102 | "expected value to be a proto message, got %T", 103 | m) 104 | } 105 | 106 | msg = pm.Proto() 107 | } 108 | 109 | result, ok := msg.(*opaqueany.Any) 110 | if !ok { 111 | return status.Errorf(codes.FailedPrecondition, "expected *opaqueany.Any, got %T", msg) 112 | } 113 | 114 | // Unmarshal 115 | return result.UnmarshalTo(out) 116 | } 117 | -------------------------------------------------------------------------------- /component/task.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | // TaskLaunchInfo is used by TaskLauncher's StartTaskFunc operation. 4 | // This type provides the details about how the new task should be configured. 5 | type TaskLaunchInfo struct { 6 | // OciUrl is a docker-run compatible specifier for an OCI image. For instance, 7 | // it supports the bare types like `ubuntu`, as well as toplevel versioned 8 | // types like `ubuntu:latest`, and any values that contain fully qualified 9 | // hostnames like `docker.io/library/ubuntu:latest`. 10 | OciUrl string 11 | 12 | // EnvironmentVariables is the set of variables that should be configured 13 | // for the task to see when it starts. 14 | EnvironmentVariables map[string]string 15 | 16 | // Entrypoint is the entrypoint override for this image. If this is not 17 | // set, then the default entrypoint for the OCI image should be used. 18 | Entrypoint []string 19 | 20 | // Arguments is passed as command line arguments to the image when it started. 21 | // If the image defines an entrypoint, then the arguments will be passed as 22 | // arguments to that program. 23 | Arguments []string 24 | } 25 | 26 | // TaskResult is the result value that must be returned by the RunTask 27 | // function. 28 | type TaskResult struct { 29 | ExitCode int 30 | } 31 | -------------------------------------------------------------------------------- /component/type_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Type -linecomment"; DO NOT EDIT. 2 | 3 | package component 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[InvalidType-0] 12 | _ = x[BuilderType-1] 13 | _ = x[RegistryType-2] 14 | _ = x[PlatformType-3] 15 | _ = x[ReleaseManagerType-4] 16 | _ = x[LogPlatformType-5] 17 | _ = x[AuthenticatorType-6] 18 | _ = x[MapperType-7] 19 | _ = x[ConfigSourcerType-8] 20 | _ = x[TaskLauncherType-9] 21 | _ = x[maxType-10] 22 | } 23 | 24 | const _Type_name = "InvalidBuilderRegistryPlatformReleaseManagerLogPlatformAuthenticatorMapperConfigSourcerTaskLaunchermaxType" 25 | 26 | var _Type_index = [...]uint8{0, 7, 14, 22, 30, 44, 55, 68, 74, 87, 99, 106} 27 | 28 | func (i Type) String() string { 29 | if i >= Type(len(_Type_index)-1) { 30 | return "Type(" + strconv.FormatInt(int64(i), 10) + ")" 31 | } 32 | return _Type_name[_Type_index[i]:_Type_index[i+1]] 33 | } 34 | -------------------------------------------------------------------------------- /datadir/app.go: -------------------------------------------------------------------------------- 1 | package datadir 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | // App is an implementation of Dir that encapsulates the directories for a 8 | // single app. 9 | type App struct { 10 | Dir 11 | } 12 | 13 | // Component returns a Dir implementation scoped to a specific component. 14 | func (d *App) Component(typ, name string) (*Component, error) { 15 | dir, err := NewScopedDir(d, filepath.Join("component", typ, name)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &Component{Dir: dir}, nil 21 | } 22 | -------------------------------------------------------------------------------- /datadir/component.go: -------------------------------------------------------------------------------- 1 | package datadir 2 | 3 | // Component is an implementation of Dir that encapsulates the directories for a 4 | // single app. 5 | type Component struct { 6 | Dir 7 | } 8 | -------------------------------------------------------------------------------- /datadir/dir.go: -------------------------------------------------------------------------------- 1 | package datadir 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // TODO(mitchellh): we use deeply nested directories here which isn't 9 | // going to work on Windows (due to MAX_PATH). We should have an alternate 10 | // implementation for Windows. 11 | 12 | // Dir is the interface implemented so that consumers can store data 13 | // locally in a consistent way. 14 | type Dir interface { 15 | // CacheDir returns the path to a folder that can be used for 16 | // cache data. This directory may not be empty if a previous run 17 | // stored data, but it may also be emptied at any time between runs. 18 | CacheDir() string 19 | 20 | // DataDir returns the path to a folder that can be used for data 21 | // that is persisted between runs. 22 | DataDir() string 23 | } 24 | 25 | // basicDir implements Dir in the simplest possible way. 26 | type basicDir struct { 27 | cacheDir string 28 | dataDir string 29 | } 30 | 31 | // CacheDir impl Dir 32 | func (d *basicDir) CacheDir() string { return d.cacheDir } 33 | 34 | // DataDir impl Dir 35 | func (d *basicDir) DataDir() string { return d.dataDir } 36 | 37 | // newRootDir creates a basicDir for the root directory which puts 38 | // data at /cache, etc. 39 | func newRootDir(path string) (Dir, error) { 40 | if err := os.MkdirAll(path, 0755); err != nil { 41 | return nil, err 42 | } 43 | 44 | cacheDir := filepath.Join(path, "cache") 45 | dataDir := filepath.Join(path, "data") 46 | if err := os.MkdirAll(cacheDir, 0755); err != nil { 47 | return nil, err 48 | } 49 | if err := os.MkdirAll(dataDir, 0755); err != nil { 50 | return nil, err 51 | } 52 | 53 | return &basicDir{cacheDir: cacheDir, dataDir: dataDir}, nil 54 | } 55 | 56 | // NewBasicDir creates a Dir implementation with a manually specified 57 | // set of directories. 58 | func NewBasicDir(cacheDir, dataDir string) Dir { 59 | return &basicDir{cacheDir: cacheDir, dataDir: dataDir} 60 | } 61 | 62 | // NewScopedDir creates a ScopedDir for the given parent at the relative 63 | // child path of path. The caller should take care that multiple scoped 64 | // dirs with overlapping paths are not created, since they could still 65 | // collide. 66 | func NewScopedDir(parent Dir, path string) (Dir, error) { 67 | cacheDir := filepath.Join(parent.CacheDir(), path) 68 | dataDir := filepath.Join(parent.DataDir(), path) 69 | if err := os.MkdirAll(cacheDir, 0755); err != nil { 70 | return nil, err 71 | } 72 | if err := os.MkdirAll(dataDir, 0755); err != nil { 73 | return nil, err 74 | } 75 | 76 | return &basicDir{cacheDir: cacheDir, dataDir: dataDir}, nil 77 | } 78 | 79 | var _ Dir = (*basicDir)(nil) 80 | -------------------------------------------------------------------------------- /datadir/dir_test.go: -------------------------------------------------------------------------------- 1 | package datadir 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewRootDir(t *testing.T) { 10 | path := "/tmp/waypoint" 11 | rootDir, err := newRootDir(path) 12 | 13 | expectedCacheDir := path + "/cache" 14 | expectedDataDir := path + "/data" 15 | 16 | assert.Nil(t, err) 17 | assert.Equal(t, rootDir.CacheDir(), expectedCacheDir) 18 | assert.Equal(t, rootDir.DataDir(), expectedDataDir) 19 | } 20 | 21 | func TestNewBasicDir(t *testing.T) { 22 | cacheDirStr := "/tmp/cache" 23 | dataDirStr := "/tmp/data" 24 | 25 | basicDir := NewBasicDir(cacheDirStr, dataDirStr) 26 | 27 | assert.Equal(t, basicDir.CacheDir(), cacheDirStr) 28 | assert.Equal(t, basicDir.DataDir(), dataDirStr) 29 | } 30 | 31 | func TestNewScopedDir(t *testing.T) { 32 | cacheDirStr := "/tmp/cache" 33 | dataDirStr := "/tmp/data" 34 | path := "/waypoint" 35 | parent := NewBasicDir(cacheDirStr, dataDirStr) 36 | 37 | scopedDir, err := NewScopedDir(parent, path) 38 | 39 | expectedCacheDir := cacheDirStr + path 40 | expectedDataDir := dataDirStr + path 41 | 42 | assert.Nil(t, err) 43 | assert.Equal(t, scopedDir.CacheDir(), expectedCacheDir) 44 | assert.Equal(t, scopedDir.DataDir(), expectedDataDir) 45 | } 46 | -------------------------------------------------------------------------------- /datadir/doc.go: -------------------------------------------------------------------------------- 1 | // Package datadir manages the data directories. This includes persisted 2 | // data such as state as well as ephemeral data such as cache and runtime 3 | // files. 4 | // 5 | // This package is aware of the data model presented and provides easy 6 | // helpers to create app-specific, component-specific, etc. data directories. 7 | // 8 | // This package is the result of lessons learned from reimplementing 9 | // "data directories" for projects such as Vagrant and Terraform. Those 10 | // projects managed a list of directories directly in the CLI, forcing 11 | // a lot of code to be aware of paths and making it hard to implement 12 | // operations on those paths such as pruning, migration, compression, etc. 13 | // As an evolution, we create the "datadir" package which has deep knowledge 14 | // of the software data model and consumers interact using higher level APIs 15 | // rather than direct filesystem manipulation. This gives us more room to 16 | // introduce improvements in the future that broadly impact the application 17 | // without having to make those changes in many places. 18 | package datadir 19 | -------------------------------------------------------------------------------- /datadir/project.go: -------------------------------------------------------------------------------- 1 | package datadir 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | // Project is an implementation of Dir that encapsulates the directory 8 | // for an entire project, including multiple apps. 9 | // 10 | // The paths returned by the Dir interface functions will be project-global. 11 | // This means that the data is shared by all applications in the project. 12 | type Project struct { 13 | Dir 14 | } 15 | 16 | // NewProject creates the directory structure for a project. This will 17 | // create the physical directories on disk if they do not already exist. 18 | func NewProject(path string) (*Project, error) { 19 | dir, err := newRootDir(path) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &Project{Dir: dir}, nil 25 | } 26 | 27 | // App returns the Dir implementation scoped to a specific app. 28 | func (p *Project) App(name string) (*App, error) { 29 | dir, err := NewScopedDir(p, filepath.Join("app", name)) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &App{Dir: dir}, nil 35 | } 36 | 37 | // Assert implementation 38 | var _ Dir = (*Project)(nil) 39 | -------------------------------------------------------------------------------- /datadir/testing.go: -------------------------------------------------------------------------------- 1 | package datadir 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/mitchellh/go-testing-interface" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // TestDir returns a Dir for testing. 12 | func TestDir(t testing.T) (Dir, func()) { 13 | t.Helper() 14 | 15 | td, err := ioutil.TempDir("", "datadir-test") 16 | require.NoError(t, err) 17 | 18 | dir, err := newRootDir(td) 19 | require.NoError(t, err) 20 | 21 | return dir, func() { os.RemoveAll(td) } 22 | } 23 | -------------------------------------------------------------------------------- /docs/docs_test.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestHiddenDocsFields(t *testing.T) { 10 | require := require.New(t) 11 | 12 | type config struct { 13 | normal string `hcl:"normal"` 14 | deprecated string `hcl:"deprecated" docs:"hidden"` // Hidden docs fields are invisible 15 | } 16 | 17 | expectedFields := map[string]*FieldDocs{ 18 | "normal": { 19 | Field: "normal", 20 | Type: "string", 21 | }, 22 | } 23 | 24 | actualFields := make(map[string]*FieldDocs) 25 | 26 | require.Nil(fromConfig(&config{}, actualFields)) 27 | 28 | require.Equal(expectedFields, actualFields) 29 | } 30 | -------------------------------------------------------------------------------- /docs/func.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/iancoleman/strcase" 8 | ) 9 | 10 | // FromFunc fills in the documentation it can from the main function 11 | // for the component: DeployFunc, ReleaseFunc, etc. v can be nil and this 12 | // will do nothing. 13 | // 14 | // This currently extracts: 15 | // 16 | // - Template fields from the result type if the result type is 17 | // a concrete type (not an interface value). 18 | func FromFunc(v interface{}) Option { 19 | return func(d *Documentation) error { 20 | v := reflect.ValueOf(v) 21 | if !v.IsValid() || v.Kind() != reflect.Func { 22 | return nil 23 | } 24 | 25 | t := v.Type() 26 | if err := funcExtractTemplateFields(d, t); err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | } 33 | 34 | // funcExtractTemplateFields extracts the templateFields values from 35 | // the given type. The type must be a struct (after all pointers are removed). 36 | // If the struct implements component.Template, we'll use that directly, 37 | // otherwise the fields will be inferred based on exported struct fields. 38 | func funcExtractTemplateFields(d *Documentation, t reflect.Type) error { 39 | // If we have no return values we can't calculate template fields. 40 | if t.NumOut() <= 0 { 41 | return nil 42 | } 43 | 44 | // Initialize our fields, even if we error later. 45 | if d.templateFields == nil { 46 | d.templateFields = map[string]*FieldDocs{} 47 | } 48 | 49 | // We get the first output type and use that. It is either a concrete 50 | // type or an error. If it is an error we'll catch it below. 51 | out := t.Out(0) 52 | for { 53 | // If the output type implements our template interface already 54 | // then we can just use that now. 55 | if out.Implements(templateType) { 56 | return funcExtractTemplateFieldsFromImpl(d, out) 57 | } 58 | 59 | // If this is a pointer, we want to unwrap it and try again. We 60 | // try again cause its possible for bare values to implement 61 | // the interface. We loop here because people could in theory 62 | // do `****struct{}` although its nonsensical. 63 | if out.Kind() == reflect.Ptr { 64 | out = out.Elem() 65 | continue 66 | } 67 | 68 | break 69 | } 70 | 71 | // We can only detect template fields on a struct. 72 | if out.Kind() != reflect.Struct { 73 | return nil 74 | } 75 | 76 | // Go through each field and document it. 77 | for i := 0; i < out.NumField(); i++ { 78 | f := out.Field(i) 79 | if f.PkgPath != "" { 80 | // Ignore unexported 81 | continue 82 | } 83 | 84 | if strings.HasPrefix(f.Name, "XXX_") { 85 | // ignore proto internals 86 | continue 87 | } 88 | 89 | // Our fields from a struct are snake case. 90 | name := strcase.ToSnake(f.Name) 91 | 92 | d.templateFields[name] = &FieldDocs{ 93 | Field: name, 94 | Type: cleanupType(f.Type.String()), 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // funcExtractTemplateFieldsFromImpl extracts the template fields from a 102 | // type that implements templateInterface. 103 | func funcExtractTemplateFieldsFromImpl(d *Documentation, t reflect.Type) error { 104 | // We need to create a new instance of t 105 | out := reflect.New(t).Elem().MethodByName("TemplateData").Call([]reflect.Value{}) 106 | fields := out[0].Interface().(map[string]interface{}) 107 | 108 | // Go through each field and document it 109 | for k, v := range fields { 110 | d.templateFields[k] = &FieldDocs{ 111 | Field: k, 112 | Type: cleanupType(reflect.TypeOf(v).String()), 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | // templateType is the type implemented by results that support 120 | // template data. We don't use component.TemplateData directly because 121 | // we are avoiding circular imports. 122 | var templateType = reflect.TypeOf((*interface { 123 | TemplateData() map[string]interface{} 124 | })(nil)).Elem() 125 | -------------------------------------------------------------------------------- /docs/func_test.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFromFunc(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Input interface{} 14 | Expected []*FieldDocs 15 | Err string 16 | }{ 17 | { 18 | "pointer to a struct", 19 | func() (*struct { 20 | Image string 21 | FullName string 22 | }, error) { 23 | return nil, nil 24 | }, 25 | []*FieldDocs{ 26 | { 27 | Field: "full_name", 28 | Type: "string", 29 | }, 30 | { 31 | Field: "image", 32 | Type: "string", 33 | }, 34 | }, 35 | "", 36 | }, 37 | 38 | { 39 | "struct implementing interface", 40 | func() (*testTemplateStruct, error) { 41 | return nil, nil 42 | }, 43 | []*FieldDocs{ 44 | { 45 | Field: "full_name", 46 | Type: "string", 47 | }, 48 | { 49 | Field: "image", 50 | Type: "string", 51 | }, 52 | }, 53 | "", 54 | }, 55 | } 56 | 57 | for _, tt := range cases { 58 | t.Run(tt.Name, func(t *testing.T) { 59 | require := require.New(t) 60 | 61 | d, err := New(FromFunc(tt.Input)) 62 | if tt.Err != "" { 63 | require.Error(err) 64 | require.Contains(err.Error(), tt.Err) 65 | return 66 | } 67 | require.NoError(err) 68 | 69 | require.Equal(tt.Expected, d.TemplateFields(), spew.Sdump(d.TemplateFields())) 70 | }) 71 | } 72 | } 73 | 74 | type testTemplateStruct struct{} 75 | 76 | func (t *testTemplateStruct) TemplateData() map[string]interface{} { 77 | return map[string]interface{}{ 78 | "image": "", 79 | "full_name": "", 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/utils.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import "strings" 4 | 5 | var typeCleanupPatterns = strings.NewReplacer( 6 | "*", "", 7 | "[]", "list of ", 8 | "map[", "map of ", 9 | "]", " to ", 10 | ) 11 | 12 | // Cleanup the type to make it nicer to read in docs. 13 | func cleanupType(t string) string { 14 | return typeCleanupPatterns.Replace(t) 15 | } 16 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1622445595, 6 | "narHash": "sha256-m+JRe6Wc5OZ/mKw2bB3+Tl0ZbtyxxxfnAWln8Q5qs+Y=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "7d706970d94bc5559077eb1a6600afddcd25a7c8", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-utils_2": { 19 | "locked": { 20 | "lastModified": 1642700792, 21 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 22 | "owner": "numtide", 23 | "repo": "flake-utils", 24 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "type": "github" 31 | } 32 | }, 33 | "nixpkgs": { 34 | "locked": { 35 | "lastModified": 1639584099, 36 | "narHash": "sha256-3UioSxWXcXMoq3YV6i7nl3DWpKX2xA3ogYnqynPBk2w=", 37 | "owner": "nixos", 38 | "repo": "nixpkgs", 39 | "rev": "9367ef512d32a3c8b9e1d2b75ba5134de48d86ed", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "nixos", 44 | "ref": "release-21.11", 45 | "repo": "nixpkgs", 46 | "type": "github" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-utils": "flake-utils", 52 | "waypoint": "waypoint" 53 | } 54 | }, 55 | "waypoint": { 56 | "inputs": { 57 | "flake-utils": "flake-utils_2", 58 | "nixpkgs": "nixpkgs" 59 | }, 60 | "locked": { 61 | "lastModified": 1650980284, 62 | "narHash": "sha256-lZk6wd77+LwBwqhi19tn6IkjfORjmXOHdSAnrF2SLlo=", 63 | "owner": "hashicorp", 64 | "repo": "waypoint", 65 | "rev": "3b57b19eadc68a0a358a1d4a7f9dad864121d134", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "hashicorp", 70 | "repo": "waypoint", 71 | "type": "github" 72 | } 73 | } 74 | }, 75 | "root": "root", 76 | "version": 7 77 | } 78 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "HashiCorp Waypoint SDK"; 3 | 4 | inputs.waypoint.url = "github:hashicorp/waypoint"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | outputs = { self, flake-utils, waypoint }: 8 | flake-utils.lib.eachDefaultSystem (system: { 9 | # Just use the exact same shell environment as Waypoint. 10 | devShell = waypoint.devShell.${system}; 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /framework/doc.go: -------------------------------------------------------------------------------- 1 | // Package framework contains a high-level framework for writing Waypoint 2 | // plugins. The framework is split into sub-packages for specific functionality, 3 | // whereas this root package contains the highest-level functionality. 4 | // 5 | // Note: the root package is still a work-in-progress. For now, please use the 6 | // sub-packages. We are building the building blocks for this package first 7 | // via the sub-packages. 8 | package framework 9 | -------------------------------------------------------------------------------- /framework/resource/doc.go: -------------------------------------------------------------------------------- 1 | // Package resource contains helpers around managing groups of resources. A 2 | // "resource" is any element created by a plugin. For example, an AWS 3 | // deployment plugin may create load balancers, security groups, VPCs, etc. Each 4 | // of these is a "resource". 5 | // 6 | // This package simplifies resource lifecycle, state management, error handling, 7 | // and more. Even if your plugin only has a single resource, this library is 8 | // highly recommended. 9 | package resource 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/waypoint-plugin-sdk 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/bgentry/speakeasy v0.1.0 7 | github.com/briandowns/spinner v1.11.1 8 | github.com/containerd/console v1.0.1 9 | github.com/creack/pty v1.1.11 10 | github.com/davecgh/go-spew v1.1.1 11 | github.com/fatih/color v1.9.0 12 | github.com/golang/protobuf v1.5.2 13 | github.com/hashicorp/go-argmapper v0.2.3 14 | github.com/hashicorp/go-hclog v0.14.1 15 | github.com/hashicorp/go-multierror v1.1.0 16 | github.com/hashicorp/go-plugin v1.4.2 17 | github.com/hashicorp/hcl/v2 v2.6.0 18 | github.com/hashicorp/opaqueany v0.0.0-20220321170339-a5c6ff5bb0ec 19 | github.com/hashicorp/protostructure v0.0.0-20220321173139-813f7b927cb7 20 | github.com/iancoleman/strcase v0.1.2 21 | github.com/kr/pretty v0.3.0 // indirect 22 | github.com/lab47/vterm v0.0.0-20201001232628-a9dd795f94c2 23 | github.com/mattn/go-colorable v0.1.8 24 | github.com/mattn/go-isatty v0.0.12 25 | github.com/mitchellh/go-glint v0.0.0-20201015034436-f80573c636de 26 | github.com/mitchellh/go-testing-interface v1.14.1 27 | github.com/mitchellh/mapstructure v1.3.3 28 | github.com/morikuni/aec v1.0.0 29 | github.com/oklog/ulid v1.3.1 30 | github.com/olekukonko/tablewriter v0.0.4 31 | github.com/stretchr/testify v1.6.1 32 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 33 | golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f 34 | google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5 35 | google.golang.org/grpc v1.33.1 36 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 37 | google.golang.org/protobuf v1.27.1 38 | ) 39 | 40 | require ( 41 | github.com/VividCortex/ewma v1.1.1 // indirect 42 | github.com/agext/levenshtein v1.2.1 // indirect 43 | github.com/apparentlymart/go-textseg v1.0.0 // indirect 44 | github.com/apparentlymart/go-textseg/v12 v12.0.0 // indirect 45 | github.com/cheggaaa/pb/v3 v3.0.5 // indirect 46 | github.com/google/go-cmp v0.5.5 // indirect 47 | github.com/gookit/color v1.3.1 // indirect 48 | github.com/hashicorp/errwrap v1.0.0 // indirect 49 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect 50 | github.com/kr/text v0.2.0 // indirect 51 | github.com/mattn/go-runewidth v0.0.7 // indirect 52 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 53 | github.com/oklog/run v1.0.0 // indirect 54 | github.com/pkg/errors v0.9.1 // indirect 55 | github.com/pmezard/go-difflib v1.0.0 // indirect 56 | github.com/rogpeppe/go-internal v1.6.1 // indirect 57 | github.com/stretchr/objx v0.2.0 // indirect 58 | github.com/tj/go-spin v1.1.0 // indirect 59 | github.com/y0ssar1an/q v1.0.7 // indirect 60 | github.com/zclconf/go-cty v1.2.0 // indirect 61 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect 62 | golang.org/x/text v0.3.2 // indirect 63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /internal-shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared Internal Packages 2 | 3 | This folder contains Go packages that are meant for internal use only. 4 | They are in this folder rather than "internal" since they are also shared 5 | with the core project. 6 | 7 | If you're writing a plugin or are any other type of external user, please 8 | do not use these packages directly. We will NOT be maintaining normal 9 | semver-style compatibility on these packages and no compatibility is 10 | guaranteed at all. 11 | -------------------------------------------------------------------------------- /internal-shared/pluginclient/client.go: -------------------------------------------------------------------------------- 1 | package pluginclient 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/go-plugin" 9 | 10 | internalplugin "github.com/hashicorp/waypoint-plugin-sdk/internal/plugin" 11 | ) 12 | 13 | // ClientConfig returns the base client config to use when connecting 14 | // to a plugin. This sets the handshake config, protocols, etc. Manually 15 | // override any values you want to set. 16 | func ClientConfig(log hclog.Logger, odr bool) *plugin.ClientConfig { 17 | odrSettings := &internalplugin.ODRSetting{Enabled: odr} 18 | 19 | return &plugin.ClientConfig{ 20 | HandshakeConfig: internalplugin.Handshake, 21 | VersionedPlugins: internalplugin.Plugins( 22 | internalplugin.WithLogger(log), 23 | internalplugin.WithODR(odrSettings), 24 | ), 25 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 26 | 27 | // We always set managed to true just in case we don't properly 28 | // call Kill so that CleanupClients gets it. If we do properly call 29 | // Kill, then it is a no-op to call it again so this is safe. 30 | Managed: true, 31 | 32 | // This is super important. There appears to be a bug with AutoMTLS 33 | // when using GRPCBroker and listening from the _client_ side. The 34 | // TLS fails to negotiate. For now we just disable this but we should 35 | // look into fixing that later. 36 | AutoMTLS: false, 37 | } 38 | } 39 | 40 | // Mappers returns the mappers supported by the plugin. 41 | func Mappers(c *plugin.Client) ([]*argmapper.Func, error) { 42 | rpcClient, err := c.Client() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | v, err := rpcClient.Dispense("mapper") 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | client, ok := v.(*internalplugin.MapperClient) 53 | if !ok { 54 | return nil, fmt.Errorf("mapper service was unexpected type: %T", v) 55 | } 56 | 57 | return client.Mappers() 58 | } 59 | -------------------------------------------------------------------------------- /internal-shared/protomappers/mappers_test.go: -------------------------------------------------------------------------------- 1 | package protomappers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/hashicorp/waypoint-plugin-sdk/component" 10 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 11 | ) 12 | 13 | func TestMappers(t *testing.T) { 14 | var cases = []struct { 15 | Name string 16 | Mapper interface{} 17 | Input []interface{} 18 | Output interface{} 19 | Error string 20 | }{ 21 | { 22 | "Source", 23 | Source, 24 | []interface{}{&pb.Args_Source{App: "foo"}}, 25 | &component.Source{App: "foo"}, 26 | "", 27 | }, 28 | 29 | { 30 | "SourceProto", 31 | SourceProto, 32 | []interface{}{&component.Source{App: "foo"}}, 33 | &pb.Args_Source{App: "foo"}, 34 | "", 35 | }, 36 | } 37 | 38 | for _, tt := range cases { 39 | t.Run(tt.Name, func(t *testing.T) { 40 | require := require.New(t) 41 | 42 | f, err := argmapper.NewFunc(tt.Mapper) 43 | require.NoError(err) 44 | 45 | var args []argmapper.Arg 46 | for _, input := range tt.Input { 47 | args = append(args, argmapper.Typed(input)) 48 | } 49 | 50 | result := f.Call(args...) 51 | if tt.Error != "" { 52 | require.Error(result.Err()) 53 | require.Contains(result.Err().Error(), tt.Error) 54 | return 55 | } 56 | require.NoError(result.Err()) 57 | require.Equal(tt.Output, result.Out(0)) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/funcspec/any_conv.go: -------------------------------------------------------------------------------- 1 | package funcspec 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/hashicorp/opaqueany" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | // anyConvGen is an argmapper.ConverterGenFunc that dynamically creates 12 | // converters to *opaqueany.Any for types that implement proto.Message. This 13 | // allows automatic conversion to *opaqueopaqueany.Any. 14 | // 15 | // This is automatically injected for all funcspec.Func calls. 16 | func anyConvGen(v argmapper.Value) (*argmapper.Func, error) { 17 | anyType := reflect.TypeOf((*opaqueany.Any)(nil)) 18 | protoMessageType := reflect.TypeOf((*proto.Message)(nil)).Elem() 19 | if !v.Type.Implements(protoMessageType) { 20 | return nil, nil 21 | } 22 | 23 | // We take this value as our input. 24 | inputSet, err := argmapper.NewValueSet([]argmapper.Value{v}) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // Generate an int with the subtype of the string value 30 | outputSet, err := argmapper.NewValueSet([]argmapper.Value{{ 31 | Name: v.Name, 32 | Type: anyType, 33 | Subtype: string(proto.MessageName(reflect.Zero(v.Type).Interface().(proto.Message))), 34 | }}) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return argmapper.BuildFunc(inputSet, outputSet, func(in, out *argmapper.ValueSet) error { 40 | anyVal, err := opaqueany.New(inputSet.Typed(v.Type).Value.Interface().(proto.Message)) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | outputSet.Typed(anyType).Value = reflect.ValueOf(anyVal) 46 | return nil 47 | }) 48 | } 49 | 50 | type protoToAny interface { 51 | Proto() *opaqueany.Any 52 | } 53 | 54 | func fromConvGen(v argmapper.Value) (*argmapper.Func, error) { 55 | anyType := reflect.TypeOf((*opaqueany.Any)(nil)) 56 | protoMessageType := reflect.TypeOf((*protoToAny)(nil)).Elem() 57 | if !v.Type.Implements(protoMessageType) { 58 | return nil, nil 59 | } 60 | 61 | // We take this value as our input. 62 | inputSet, err := argmapper.NewValueSet([]argmapper.Value{v}) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // Generate an int with the subtype of the string value 68 | outputSet, err := argmapper.NewValueSet([]argmapper.Value{{ 69 | Name: v.Name, 70 | Type: anyType, 71 | Subtype: string(proto.MessageName(reflect.Zero(v.Type).Interface().(proto.Message))), 72 | }}) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return argmapper.BuildFunc(inputSet, outputSet, func(in, out *argmapper.ValueSet) error { 78 | anyVal, err := opaqueany.New(inputSet.Typed(v.Type).Value.Interface().(proto.Message)) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | outputSet.Typed(anyType).Value = reflect.ValueOf(anyVal) 84 | return nil 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /internal/funcspec/args.go: -------------------------------------------------------------------------------- 1 | package funcspec 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/opaqueany" 9 | 10 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 11 | ) 12 | 13 | // Args is a type that will be populated with all the expected args of 14 | // the FuncSpec. This can be used in the callback (cb) to Func. 15 | type Args []*pb.FuncSpec_Value 16 | 17 | // appendValue appends an argmapper.Value to Args. The value must be an *opaqueany.Any 18 | // or a supported primitive value. If an invalid value is given this will panic. 19 | func appendValue(args Args, v argmapper.Value) Args { 20 | value := &pb.FuncSpec_Value{ 21 | Name: v.Name, 22 | Type: v.Subtype, 23 | } 24 | 25 | // If we have no value, use the zero value of the type. This can happen 26 | // when we're given a Value from an arg type, not a function call. 27 | if !v.Value.IsValid() { 28 | v.Value = reflect.Zero(v.Type) 29 | } 30 | 31 | switch v := v.Value.Interface().(type) { 32 | case *opaqueany.Any: 33 | value.Value = &pb.FuncSpec_Value_ProtoAny{ProtoAny: v} 34 | 35 | case bool: 36 | value.PrimitiveType = pb.FuncSpec_Value_BOOL 37 | value.Value = &pb.FuncSpec_Value_Bool{Bool: v} 38 | 39 | case int: 40 | value.PrimitiveType = pb.FuncSpec_Value_INT 41 | value.Value = &pb.FuncSpec_Value_Int{Int: int64(v)} 42 | case int8: 43 | value.PrimitiveType = pb.FuncSpec_Value_INT8 44 | value.Value = &pb.FuncSpec_Value_Int{Int: int64(v)} 45 | case int16: 46 | value.PrimitiveType = pb.FuncSpec_Value_INT16 47 | value.Value = &pb.FuncSpec_Value_Int{Int: int64(v)} 48 | case int32: 49 | value.PrimitiveType = pb.FuncSpec_Value_INT32 50 | value.Value = &pb.FuncSpec_Value_Int{Int: int64(v)} 51 | case int64: 52 | value.PrimitiveType = pb.FuncSpec_Value_INT64 53 | value.Value = &pb.FuncSpec_Value_Int{Int: int64(v)} 54 | 55 | case uint: 56 | value.PrimitiveType = pb.FuncSpec_Value_UINT 57 | value.Value = &pb.FuncSpec_Value_Uint{Uint: uint64(v)} 58 | case uint8: 59 | value.PrimitiveType = pb.FuncSpec_Value_UINT8 60 | value.Value = &pb.FuncSpec_Value_Uint{Uint: uint64(v)} 61 | case uint16: 62 | value.PrimitiveType = pb.FuncSpec_Value_UINT16 63 | value.Value = &pb.FuncSpec_Value_Uint{Uint: uint64(v)} 64 | case uint32: 65 | value.PrimitiveType = pb.FuncSpec_Value_UINT32 66 | value.Value = &pb.FuncSpec_Value_Uint{Uint: uint64(v)} 67 | case uint64: 68 | value.PrimitiveType = pb.FuncSpec_Value_UINT64 69 | value.Value = &pb.FuncSpec_Value_Uint{Uint: uint64(v)} 70 | 71 | case string: 72 | value.PrimitiveType = pb.FuncSpec_Value_STRING 73 | value.Value = &pb.FuncSpec_Value_String_{String_: v} 74 | 75 | default: 76 | panic(fmt.Sprintf("invalid value type for args: %T", v)) 77 | } 78 | 79 | return append(args, value) 80 | } 81 | -------------------------------------------------------------------------------- /internal/funcspec/func_test.go: -------------------------------------------------------------------------------- 1 | package funcspec 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/opaqueany" 9 | "github.com/stretchr/testify/require" 10 | empty "google.golang.org/protobuf/types/known/emptypb" 11 | 12 | "github.com/hashicorp/go-hclog" 13 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 14 | ) 15 | 16 | func init() { 17 | hclog.L().SetLevel(hclog.Trace) 18 | } 19 | 20 | func TestFunc(t *testing.T) { 21 | t.Run("single any result", func(t *testing.T) { 22 | require := require.New(t) 23 | 24 | spec, err := Spec(func(*empty.Empty) *empty.Empty { return &empty.Empty{} }) 25 | require.NoError(err) 26 | require.NotNil(spec) 27 | 28 | f := Func(spec, func(args Args) (*opaqueany.Any, error) { 29 | require.Len(args, 1) 30 | require.NotNil(args[0]) 31 | 32 | // At this point we'd normally RPC out. 33 | return opaqueany.New(&empty.Empty{}) 34 | }) 35 | 36 | msg, err := opaqueany.New(&empty.Empty{}) 37 | require.NoError(err) 38 | 39 | name := string((&empty.Empty{}).ProtoReflect().Descriptor().FullName()) 40 | result := f.Call(argmapper.TypedSubtype(msg, name)) 41 | require.NoError(result.Err()) 42 | require.Equal(reflect.Struct, reflect.ValueOf(result.Out(0)).Kind()) 43 | }) 44 | 45 | t.Run("single missing requirement", func(t *testing.T) { 46 | require := require.New(t) 47 | 48 | spec, err := Spec(func(*empty.Empty) *empty.Empty { return &empty.Empty{} }) 49 | require.NoError(err) 50 | require.NotNil(spec) 51 | 52 | f := Func(spec, func(args Args) (*opaqueany.Any, error) { 53 | require.Len(args, 1) 54 | require.NotNil(args[0]) 55 | 56 | // At this point we'd normally RPC out. 57 | return opaqueany.New(&empty.Empty{}) 58 | }) 59 | 60 | // Create an argument with the wrong type 61 | msg, err := opaqueany.New(&empty.Empty{}) 62 | require.NoError(err) 63 | name := string((&pb.FuncSpec{}).ProtoReflect().Descriptor().FullName()) 64 | result := f.Call(argmapper.TypedSubtype(msg, name)) 65 | 66 | // We should have an error 67 | require.Error(result.Err()) 68 | require.IsType(result.Err(), &argmapper.ErrArgumentUnsatisfied{}) 69 | }) 70 | 71 | t.Run("match callback output if no results", func(t *testing.T) { 72 | require := require.New(t) 73 | 74 | spec, err := Spec(func(*empty.Empty) *empty.Empty { return &empty.Empty{} }) 75 | require.NoError(err) 76 | require.NotNil(spec) 77 | 78 | // No results 79 | spec.Result = nil 80 | 81 | // Build our func to return a primitive 82 | f := Func(spec, func(args Args) int { 83 | require.Len(args, 1) 84 | require.NotNil(args[0]) 85 | return 42 86 | }) 87 | 88 | // Call the function with the proto type we expect 89 | msg, err := opaqueany.New(&empty.Empty{}) 90 | require.NoError(err) 91 | 92 | name := string((&empty.Empty{}).ProtoReflect().Descriptor().FullName()) 93 | result := f.Call(argmapper.TypedSubtype(msg, name)) 94 | 95 | // Should succeed and give us our primitive 96 | require.NoError(result.Err()) 97 | require.Equal(42, result.Out(0)) 98 | }) 99 | 100 | t.Run("provide input arguments", func(t *testing.T) { 101 | require := require.New(t) 102 | 103 | spec, err := Spec(func(*empty.Empty) *empty.Empty { return &empty.Empty{} }) 104 | require.NoError(err) 105 | require.NotNil(spec) 106 | 107 | f := Func(spec, func(args Args, v int) (*opaqueany.Any, error) { 108 | require.Len(args, 2) 109 | require.NotNil(args[0]) 110 | require.NotNil(args[1]) 111 | require.Equal(42, v) 112 | 113 | // At this point we'd normally RPC out. 114 | return opaqueany.New(&empty.Empty{}) 115 | }, argmapper.Typed(int(42))) 116 | 117 | msg, err := opaqueany.New(&empty.Empty{}) 118 | require.NoError(err) 119 | 120 | name := string((&empty.Empty{}).ProtoReflect().Descriptor().FullName()) 121 | result := f.Call(argmapper.TypedSubtype(msg, name)) 122 | require.NoError(result.Err()) 123 | require.Equal(reflect.Struct, reflect.ValueOf(result.Out(0)).Kind()) 124 | }) 125 | 126 | t.Run("primitive arguments", func(t *testing.T) { 127 | require := require.New(t) 128 | 129 | spec, err := Spec(func(v bool) *empty.Empty { return &empty.Empty{} }) 130 | require.NoError(err) 131 | require.NotNil(spec) 132 | 133 | f := Func(spec, func(args Args, v int) (*opaqueany.Any, error) { 134 | require.Len(args, 2) 135 | require.NotNil(args[0]) 136 | require.NotNil(args[1]) 137 | require.Equal(42, v) 138 | 139 | // At this point we'd normally RPC out. 140 | return opaqueany.New(&empty.Empty{}) 141 | }, argmapper.Typed(int(42))) 142 | 143 | result := f.Call(argmapper.Typed(true)) 144 | require.NoError(result.Err()) 145 | require.Equal(reflect.Struct, reflect.ValueOf(result.Out(0)).Kind()) 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /internal/funcspec/spec.go: -------------------------------------------------------------------------------- 1 | package funcspec 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | "google.golang.org/protobuf/proto" 11 | 12 | "github.com/hashicorp/waypoint-plugin-sdk/component" 13 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 14 | ) 15 | 16 | // Spec takes a function pointer and generates a FuncSpec from it. The 17 | // function must only take arguments that are proto.Message implementations 18 | // or have a chain of converters that directly convert to a proto.Message. 19 | func Spec(fn interface{}, args ...argmapper.Arg) (*pb.FuncSpec, error) { 20 | if fn == nil { 21 | return nil, status.Errorf(codes.Unimplemented, "required plugin type not implemented") 22 | } 23 | 24 | filterProto := argmapper.FilterType(protoMessageType) 25 | 26 | // Outparameters do not need to be supplied by core, and should 27 | // be omitted from the advertised function spec. 28 | filterOutParameter := argmapper.FilterType(outParameterType) 29 | 30 | // Copy our args cause we're going to use append() and we don't 31 | // want to modify our caller. 32 | args = append([]argmapper.Arg{ 33 | argmapper.FilterOutput(filterProto), 34 | }, args...) 35 | 36 | // Build our function 37 | f, err := argmapper.NewFunc(fn) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | filter := argmapper.FilterOr( 43 | argmapper.FilterType(contextType), 44 | filterPrimitive, 45 | filterProto, 46 | filterOutParameter, 47 | ) 48 | 49 | // Redefine the function in terms of protobuf messages. "Redefine" changes 50 | // the inputs of a function to only require values that match our filter 51 | // function. In our case, that is protobuf messages. 52 | f, err = f.Redefine(append(args, 53 | argmapper.FilterInput(filter), 54 | )...) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | // Grab the input set of the function and build up our funcspec 60 | result := pb.FuncSpec{Name: f.Name()} 61 | for _, v := range f.Input().Values() { 62 | if !filterProto(v) && !filterPrimitive(v) { 63 | continue 64 | } 65 | 66 | val := &pb.FuncSpec_Value{Name: v.Name} 67 | switch { 68 | case filterProto(v): 69 | val.Type = typeToMessage(v.Type) 70 | 71 | case filterPrimitive(v): 72 | val.PrimitiveType = pb.FuncSpec_Value_PrimitiveType(v.Type.Kind()) 73 | } 74 | 75 | result.Args = append(result.Args, val) 76 | } 77 | 78 | // Grab the output set and store that 79 | for _, v := range f.Output().Values() { 80 | // We only advertise proto types in output since those are the only 81 | // types we can send across the plugin boundary. 82 | if !filterProto(v) { 83 | continue 84 | } 85 | 86 | result.Result = append(result.Result, &pb.FuncSpec_Value{ 87 | Name: v.Name, 88 | Type: typeToMessage(v.Type), 89 | }) 90 | } 91 | 92 | return &result, nil 93 | } 94 | 95 | func typeToMessage(typ reflect.Type) string { 96 | val := reflect.Zero(typ).Interface().(proto.Message) 97 | return string(val.ProtoReflect().Descriptor().FullName()) 98 | } 99 | 100 | func filterPrimitive(v argmapper.Value) bool { 101 | _, ok := validPrimitive[v.Type.Kind()] 102 | return ok 103 | } 104 | 105 | var ( 106 | contextType = reflect.TypeOf((*context.Context)(nil)).Elem() 107 | protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem() 108 | outParameterType = reflect.TypeOf((*component.OutParameter)(nil)).Elem() 109 | 110 | // validPrimitive is the map of primitive types we support coming 111 | // over the plugin boundary. To add a new type to this, you must 112 | // update: 113 | // 114 | // 1. the Primitive enum in plugin.proto 115 | // 2. appendValue in args.go 116 | // 3. value.Type setting in func.go Func 117 | // 4. arg decoding in internal/plugin/dynamic_call.go 118 | // 119 | validPrimitive = map[reflect.Kind]struct{}{ 120 | reflect.Bool: {}, 121 | reflect.Int: {}, 122 | reflect.Int8: {}, 123 | reflect.Int16: {}, 124 | reflect.Int32: {}, 125 | reflect.Int64: {}, 126 | reflect.Uint: {}, 127 | reflect.Uint8: {}, 128 | reflect.Uint16: {}, 129 | reflect.Uint32: {}, 130 | reflect.Uint64: {}, 131 | reflect.String: {}, 132 | } 133 | ) 134 | -------------------------------------------------------------------------------- /internal/pkg/conpty/conpty.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | // Original copyright 2020 ActiveState Software. All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file 7 | 8 | package conpty 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "golang.org/x/sys/windows" 15 | ) 16 | 17 | // ConPty represents a windows pseudo console. 18 | type ConPty struct { 19 | hpCon windows.Handle 20 | pipeFdIn windows.Handle 21 | pipeFdOut windows.Handle 22 | consoleSize uintptr 23 | inPipe *os.File 24 | outPipe *os.File 25 | } 26 | 27 | // New returns a new ConPty pseudo terminal device 28 | func New(columns int16, rows int16) (*ConPty, error) { 29 | c := &ConPty{ 30 | consoleSize: uintptr(columns) + (uintptr(rows) << 16), 31 | } 32 | 33 | return c, c.createPseudoConsoleAndPipes() 34 | } 35 | 36 | // Close closes the pseudo-terminal and cleans up all attached resources 37 | func (c *ConPty) Close() error { 38 | err := closePseudoConsole(c.hpCon) 39 | c.inPipe.Close() 40 | c.outPipe.Close() 41 | return err 42 | } 43 | 44 | // OutPipe returns the output pipe of the pseudo terminal 45 | func (c *ConPty) OutPipe() *os.File { 46 | return c.outPipe 47 | } 48 | 49 | // InPipe returns input pipe of the pseudo terminal 50 | // Note: It is safer to use the Write method to prevent partially-written VT sequences 51 | // from corrupting the terminal 52 | func (c *ConPty) InPipe() *os.File { 53 | return c.inPipe 54 | } 55 | 56 | func (c *ConPty) createPseudoConsoleAndPipes() error { 57 | // These are the readers/writers for "stdin", but we only need this to 58 | // successfully call CreatePseudoConsole. After, we can throw it away. 59 | var hPipeInW, hPipeInR windows.Handle 60 | 61 | // Create the stdin pipe although we never use this. 62 | if err := windows.CreatePipe(&hPipeInR, &hPipeInW, nil, 0); err != nil { 63 | return err 64 | } 65 | 66 | // Create the stdout pipe 67 | if err := windows.CreatePipe(&c.pipeFdOut, &c.pipeFdIn, nil, 0); err != nil { 68 | return err 69 | } 70 | 71 | // Create the pty with our stdin/stdout 72 | if err := createPseudoConsole(c.consoleSize, hPipeInR, c.pipeFdIn, &c.hpCon); err != nil { 73 | return fmt.Errorf("failed to create pseudo console: %d, %v", uintptr(c.hpCon), err) 74 | } 75 | 76 | // Close our stdin cause we're never going to use it 77 | if hPipeInR != windows.InvalidHandle { 78 | windows.CloseHandle(hPipeInR) 79 | } 80 | if hPipeInW != windows.InvalidHandle { 81 | windows.CloseHandle(hPipeInW) 82 | } 83 | 84 | c.inPipe = os.NewFile(uintptr(c.pipeFdIn), "|0") 85 | c.outPipe = os.NewFile(uintptr(c.pipeFdOut), "|1") 86 | 87 | return nil 88 | } 89 | 90 | func (c *ConPty) Resize(cols uint16, rows uint16) error { 91 | return resizePseudoConsole(c.hpCon, uintptr(cols)+(uintptr(rows)<<16)) 92 | } 93 | -------------------------------------------------------------------------------- /internal/pkg/conpty/syscall.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | // Copyright 2020 ActiveState Software. All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file 7 | 8 | package conpty 9 | 10 | import ( 11 | "unsafe" 12 | 13 | "golang.org/x/sys/windows" 14 | ) 15 | 16 | var ( 17 | kernel32 = windows.NewLazySystemDLL("kernel32.dll") 18 | procResizePseudoConsole = kernel32.NewProc("ResizePseudoConsole") 19 | procCreatePseudoConsole = kernel32.NewProc("CreatePseudoConsole") 20 | procClosePseudoConsole = kernel32.NewProc("ClosePseudoConsole") 21 | ) 22 | 23 | func createPseudoConsole(consoleSize uintptr, ptyIn windows.Handle, ptyOut windows.Handle, hpCon *windows.Handle) (err error) { 24 | r1, _, e1 := procCreatePseudoConsole.Call( 25 | consoleSize, 26 | uintptr(ptyIn), 27 | uintptr(ptyOut), 28 | 0, 29 | uintptr(unsafe.Pointer(hpCon)), 30 | ) 31 | 32 | if r1 != 0 { // !S_OK 33 | err = e1 34 | } 35 | return 36 | } 37 | 38 | func resizePseudoConsole(handle windows.Handle, consoleSize uintptr) (err error) { 39 | r1, _, e1 := procResizePseudoConsole.Call(uintptr(handle), consoleSize) 40 | if r1 != 0 { // !S_OK 41 | err = e1 42 | } 43 | return 44 | } 45 | 46 | func closePseudoConsole(handle windows.Handle) (err error) { 47 | r1, _, e1 := procClosePseudoConsole.Call(uintptr(handle)) 48 | if r1 == 0 { 49 | err = e1 50 | } 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /internal/pkg/pty/pty.go: -------------------------------------------------------------------------------- 1 | package pty 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Pty is the minimal pseudo-tty interface we require. 8 | type Pty interface { 9 | InPipe() *os.File 10 | OutPipe() *os.File 11 | Resize(cols uint16, rows uint16) error 12 | Close() error 13 | } 14 | 15 | // New creates a new Pty. 16 | func New() (Pty, error) { 17 | return newPty() 18 | } 19 | -------------------------------------------------------------------------------- /internal/pkg/pty/pty_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package pty 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/creack/pty" 10 | ) 11 | 12 | func newPty() (Pty, error) { 13 | pty, tty, err := pty.Open() 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return &unixPty{ 19 | pty: pty, 20 | tty: tty, 21 | }, nil 22 | } 23 | 24 | type unixPty struct { 25 | pty, tty *os.File 26 | } 27 | 28 | func (p *unixPty) InPipe() *os.File { 29 | return p.tty 30 | } 31 | 32 | func (p *unixPty) OutPipe() *os.File { 33 | return p.pty 34 | } 35 | 36 | func (p *unixPty) Resize(cols uint16, rows uint16) error { 37 | return pty.Setsize(p.tty, &pty.Winsize{ 38 | Rows: rows, 39 | Cols: cols, 40 | }) 41 | } 42 | 43 | func (p *unixPty) Close() error { 44 | p.pty.Close() 45 | p.tty.Close() 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/pkg/pty/pty_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package pty 5 | 6 | import ( 7 | "os" 8 | 9 | "golang.org/x/sys/windows" 10 | 11 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pkg/conpty" 12 | ) 13 | 14 | func newPty() (Pty, error) { 15 | // We use the CreatePseudoConsole API which was introduced in build 17763 16 | vsn := windows.RtlGetVersion() 17 | if vsn.MajorVersion < 10 || 18 | vsn.BuildNumber < 17763 { 19 | return pipePty() 20 | } 21 | 22 | return conpty.New(80, 80) 23 | } 24 | 25 | func pipePty() (Pty, error) { 26 | r, w, err := os.Pipe() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &pipePtyVal{r: r, w: w}, nil 32 | } 33 | 34 | type pipePtyVal struct { 35 | r, w *os.File 36 | } 37 | 38 | func (p *pipePtyVal) InPipe() *os.File { 39 | return p.w 40 | } 41 | 42 | func (p *pipePtyVal) OutPipe() *os.File { 43 | return p.r 44 | } 45 | 46 | func (p *pipePtyVal) Resize(uint16, uint16) error { 47 | return nil 48 | } 49 | 50 | func (p *pipePtyVal) Close() error { 51 | p.w.Close() 52 | p.r.Close() 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/spinner/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | .idea 29 | *.iml 30 | -------------------------------------------------------------------------------- /internal/pkg/spinner/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13 4 | - 1.14.1 5 | env: 6 | - GOARCH: amd64 7 | - GOARCH: 386 8 | script: 9 | - go test -v 10 | notifications: 11 | email: 12 | recipients: 13 | - brian.downs@gmail.com 14 | on_success: change 15 | on_failure: always 16 | -------------------------------------------------------------------------------- /internal/pkg/spinner/Makefile: -------------------------------------------------------------------------------- 1 | GO = GO111MODULE=on GOFLAGS=-mod=vendor go 2 | 3 | .PHONY: deps 4 | deps: 5 | $(GO) mod download 6 | $(GO) mod vendor 7 | 8 | .PHONY: test 9 | test: 10 | $(GO) test -v -cover ./... 11 | 12 | .PHONY: check 13 | check: 14 | if [ -d vendor ]; then cp -r vendor/* ${GOPATH}/src/; fi 15 | 16 | .PHONY: clean 17 | clean: 18 | $(GO) clean 19 | -------------------------------------------------------------------------------- /internal/pkg/spinner/_example/main.go: -------------------------------------------------------------------------------- 1 | // Example application that uses all of the available API options. 2 | package main 3 | 4 | import ( 5 | "log" 6 | "time" 7 | 8 | "github.com/briandowns/spinner" 9 | ) 10 | 11 | func main() { 12 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) // Build our new spinner 13 | s.Color("red") // Set the spinner color to red 14 | s.Start() // Start the spinner 15 | time.Sleep(4 * time.Second) // Run for some time to simulate work 16 | 17 | s.UpdateCharSet(spinner.CharSets[9]) // Update spinner to use a different character set 18 | s.UpdateSpeed(100 * time.Millisecond) // Update the speed the spinner spins at 19 | 20 | s.Prefix = "prefixed text: " // Prefix text before the spinner 21 | time.Sleep(4 * time.Second) 22 | s.Prefix = "" 23 | s.Suffix = " :appended text" // Append text after the spinner 24 | time.Sleep(4 * time.Second) 25 | 26 | s.Prefix = "Colors: " 27 | 28 | if err := s.Color("yellow"); err != nil { 29 | log.Fatalln(err) 30 | } 31 | 32 | s.Start() 33 | 34 | time.Sleep(4 * time.Second) // Run for some time to simulate work 35 | 36 | if err := s.Color("red"); err != nil { 37 | log.Fatalln(err) 38 | } 39 | 40 | s.UpdateCharSet(spinner.CharSets[20]) 41 | s.Reverse() 42 | s.Restart() 43 | 44 | time.Sleep(4 * time.Second) // Run for some time to simulate work 45 | 46 | if err := s.Color("blue"); err != nil { 47 | log.Fatalln(err) 48 | } 49 | 50 | s.UpdateCharSet(spinner.CharSets[3]) 51 | 52 | s.Restart() 53 | 54 | time.Sleep(4 * time.Second) // Run for some time to simulate work 55 | 56 | if err := s.Color("cyan"); err != nil { 57 | log.Fatalln(err) 58 | } 59 | 60 | s.UpdateCharSet(spinner.CharSets[28]) 61 | 62 | s.Reverse() 63 | 64 | s.Restart() 65 | 66 | time.Sleep(4 * time.Second) // Run for some time to simulate work 67 | 68 | if err := s.Color("green"); err != nil { 69 | log.Fatalln(err) 70 | } 71 | 72 | s.UpdateCharSet(spinner.CharSets[25]) 73 | 74 | s.Restart() 75 | 76 | time.Sleep(4 * time.Second) // Run for some time to simulate work 77 | 78 | if err := s.Color("magenta"); err != nil { 79 | log.Fatalln(err) 80 | } 81 | 82 | s.UpdateCharSet(spinner.CharSets[32]) 83 | 84 | s.Restart() 85 | 86 | time.Sleep(4 * time.Second) // Run for some time to simulate work 87 | 88 | if err := s.Color("white"); err != nil { 89 | log.Fatalln(err) 90 | } 91 | 92 | s.FinalMSG = "Complete!\nNew line!\nAnother one!\n" 93 | 94 | s.UpdateCharSet(spinner.CharSets[31]) 95 | 96 | s.Restart() 97 | 98 | time.Sleep(4 * time.Second) // Run for some time to simulate work 99 | 100 | s.Stop() // Stop the spinner 101 | 102 | s.Prefix = "Earth! " 103 | s.UpdateCharSet(spinner.CharSets[39]) 104 | 105 | s.Restart() 106 | 107 | time.Sleep(4 * time.Second) // Run for some time to simulate work 108 | 109 | s.Stop() // Stop the spinner 110 | 111 | println("") 112 | } 113 | -------------------------------------------------------------------------------- /internal/plugin/base.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/go-argmapper" 5 | "github.com/hashicorp/go-hclog" 6 | "github.com/hashicorp/go-plugin" 7 | 8 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 9 | ) 10 | 11 | // base contains shared logic for all plugins. This should be embedded 12 | // in every plugin implementation. 13 | type base struct { 14 | Broker *plugin.GRPCBroker 15 | Logger hclog.Logger 16 | Mappers []*argmapper.Func 17 | } 18 | 19 | // internal returns a new pluginargs.Internal that can be used with 20 | // dynamic calls. The Internal structure is an internal-only argument 21 | // that is used to perform cleanup. 22 | func (b *base) internal() *pluginargs.Internal { 23 | return &pluginargs.Internal{ 24 | Broker: b.Broker, 25 | Mappers: b.Mappers, 26 | Cleanup: &pluginargs.Cleanup{}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/plugin/builder_mix.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/waypoint-plugin-sdk/component" 5 | ) 6 | 7 | type mix_Builder_Authenticator struct { 8 | component.Authenticator 9 | component.ConfigurableNotify 10 | component.Builder 11 | component.Documented 12 | } 13 | -------------------------------------------------------------------------------- /internal/plugin/builder_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/hashicorp/opaqueany" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/hashicorp/waypoint-plugin-sdk/component" 14 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 15 | "github.com/hashicorp/waypoint-plugin-sdk/internal/testproto" 16 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 17 | ) 18 | 19 | func TestBuilderBuild(t *testing.T) { 20 | require := require.New(t) 21 | assert := assert.New(t) 22 | 23 | called := false 24 | buildFunc := func(ctx context.Context, args *component.Source) *testproto.Data { 25 | called = true 26 | assert.NotNil(ctx) 27 | assert.Equal("foo", args.App) 28 | return &testproto.Data{Value: "hello"} 29 | } 30 | 31 | mockB := &mocks.Builder{} 32 | mockB.On("BuildFunc").Return(buildFunc) 33 | 34 | plugins := Plugins(WithComponents(mockB), WithMappers(testDefaultMappers(t)...)) 35 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 36 | defer client.Close() 37 | defer server.Stop() 38 | 39 | raw, err := client.Dispense("builder") 40 | require.NoError(err) 41 | builder := raw.(component.Builder) 42 | f := builder.BuildFunc().(*argmapper.Func) 43 | require.NotNil(f) 44 | 45 | result := f.Call( 46 | argmapper.Typed(context.Background()), 47 | argmapper.Typed(&pb.Args_Source{App: "foo"}), 48 | ) 49 | require.NoError(result.Err()) 50 | 51 | raw = result.Out(0) 52 | require.NotNil(raw) 53 | require.Implements((*component.Artifact)(nil), raw) 54 | 55 | anyVal := raw.(component.ProtoMarshaler).Proto().(*opaqueany.Any) 56 | name := anyVal.MessageName() 57 | require.NoError(err) 58 | require.Equal("testproto.Data", string(name)) 59 | 60 | require.True(called) 61 | } 62 | 63 | func TestBuilderDynamicFunc_auth(t *testing.T) { 64 | testDynamicFunc(t, "builder", &mockBuilderAuthenticator{}, func(v, f interface{}) { 65 | v.(*mockBuilderAuthenticator).Authenticator.On("AuthFunc").Return(f) 66 | }, func(raw interface{}) interface{} { 67 | return raw.(component.Authenticator).AuthFunc() 68 | }) 69 | } 70 | 71 | func TestBuilderDynamicFunc_validateAuth(t *testing.T) { 72 | testDynamicFunc(t, "builder", &mockBuilderAuthenticator{}, func(v, f interface{}) { 73 | v.(*mockBuilderAuthenticator).Authenticator.On("ValidateAuthFunc").Return(f) 74 | }, func(raw interface{}) interface{} { 75 | return raw.(component.Authenticator).ValidateAuthFunc() 76 | }) 77 | } 78 | 79 | func TestBuilderConfig(t *testing.T) { 80 | mockV := &mockBuilderConfigurable{} 81 | testConfigurable(t, "builder", mockV, &mockV.Configurable) 82 | } 83 | 84 | type mockBuilderAuthenticator struct { 85 | mocks.Builder 86 | mocks.Authenticator 87 | } 88 | 89 | type mockBuilderConfigurable struct { 90 | mocks.Builder 91 | mocks.Configurable 92 | } 93 | -------------------------------------------------------------------------------- /internal/plugin/config_sourcer_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/hashicorp/waypoint-plugin-sdk/component" 13 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 14 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 15 | ) 16 | 17 | func TestConfigSourcerRead(t *testing.T) { 18 | require := require.New(t) 19 | assert := assert.New(t) 20 | 21 | called := false 22 | readFunc := func(ctx context.Context) []*pb.ConfigSource_Value { 23 | called = true 24 | assert.NotNil(ctx) 25 | return []*pb.ConfigSource_Value{ 26 | { 27 | Name: "hello", 28 | }, 29 | } 30 | } 31 | 32 | mockB := &mocks.ConfigSourcer{} 33 | mockB.On("ReadFunc").Return(readFunc) 34 | 35 | plugins := Plugins(WithComponents(mockB), WithMappers(testDefaultMappers(t)...)) 36 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 37 | defer client.Close() 38 | defer server.Stop() 39 | 40 | raw, err := client.Dispense("configsourcer") 41 | require.NoError(err) 42 | source := raw.(component.ConfigSourcer) 43 | f := source.ReadFunc().(*argmapper.Func) 44 | require.NotNil(f) 45 | 46 | result := f.Call( 47 | argmapper.Typed(context.Background()), 48 | ) 49 | require.NoError(result.Err()) 50 | 51 | raw = result.Out(0) 52 | require.NotNil(raw) 53 | 54 | values := raw.([]*pb.ConfigSource_Value) 55 | require.Len(values, 1) 56 | 57 | require.True(called) 58 | } 59 | 60 | func TestConfigSourcerStop(t *testing.T) { 61 | require := require.New(t) 62 | assert := assert.New(t) 63 | 64 | called := false 65 | stopFunc := func(ctx context.Context) error { 66 | called = true 67 | assert.NotNil(ctx) 68 | return nil 69 | } 70 | 71 | mockB := &mocks.ConfigSourcer{} 72 | mockB.On("StopFunc").Return(stopFunc) 73 | 74 | plugins := Plugins(WithComponents(mockB), WithMappers(testDefaultMappers(t)...)) 75 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 76 | defer client.Close() 77 | defer server.Stop() 78 | 79 | raw, err := client.Dispense("configsourcer") 80 | require.NoError(err) 81 | source := raw.(component.ConfigSourcer) 82 | f := source.StopFunc().(*argmapper.Func) 83 | require.NotNil(f) 84 | 85 | result := f.Call( 86 | argmapper.Typed(context.Background()), 87 | ) 88 | require.NoError(result.Err()) 89 | 90 | require.True(called) 91 | } 92 | 93 | func TestConfigSourcerConfig(t *testing.T) { 94 | mockV := &mockConfigSourcerConfigurable{} 95 | testConfigurable(t, "configsourcer", mockV, &mockV.Configurable) 96 | } 97 | 98 | type mockConfigSourcerAuthenticator struct { 99 | mocks.ConfigSourcer 100 | mocks.Authenticator 101 | } 102 | 103 | type mockConfigSourcerConfigurable struct { 104 | mocks.ConfigSourcer 105 | mocks.Configurable 106 | } 107 | -------------------------------------------------------------------------------- /internal/plugin/configure_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | "github.com/hashicorp/hcl/v2/hclparse" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/hashicorp/waypoint-plugin-sdk/component" 11 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 12 | ) 13 | 14 | // testConfigurable is a reusable helper that tests that a component implements 15 | // the Configurable interfaces correctly. 16 | func testConfigurable( 17 | t *testing.T, 18 | typ string, // plugin type 19 | impl interface{}, // full implementation 20 | mockC *mocks.Configurable, 21 | ) { 22 | require := require.New(t) 23 | 24 | var config struct { 25 | Name string `hcl:"name"` 26 | } 27 | mockC.On("Config").Return(&config, nil) 28 | 29 | plugins := Plugins(WithComponents(impl), WithMappers(testDefaultMappers(t)...)) 30 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 31 | defer client.Close() 32 | defer server.Stop() 33 | 34 | raw, err := client.Dispense(typ) 35 | require.NoError(err) 36 | 37 | src := `name = "foo"` 38 | f, diag := hclparse.NewParser().ParseHCL([]byte(src), "test.hcl") 39 | require.False(diag.HasErrors()) 40 | 41 | diag = component.Configure(raw, f.Body, nil) 42 | require.False(diag.HasErrors()) 43 | require.Equal("foo", config.Name) 44 | } 45 | -------------------------------------------------------------------------------- /internal/plugin/destroyer.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/go-plugin" 9 | "google.golang.org/grpc" 10 | empty "google.golang.org/protobuf/types/known/emptypb" 11 | 12 | "github.com/hashicorp/waypoint-plugin-sdk/component" 13 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 14 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 15 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 16 | ) 17 | 18 | // destroyerClient implements component.Destroyer for a service that 19 | // has the destroy methods implemented. 20 | type destroyerClient struct { 21 | Client destroyerProtoClient 22 | Logger hclog.Logger 23 | Broker *plugin.GRPCBroker 24 | Mappers []*argmapper.Func 25 | } 26 | 27 | func (c *destroyerClient) Implements(ctx context.Context) (bool, error) { 28 | if c == nil { 29 | return false, nil 30 | } 31 | 32 | resp, err := c.Client.IsDestroyer(ctx, &empty.Empty{}) 33 | if err != nil { 34 | return false, err 35 | } 36 | 37 | return resp.Implements, nil 38 | } 39 | 40 | func (c *destroyerClient) DestroyFunc() interface{} { 41 | impl, err := c.Implements(context.Background()) 42 | if err != nil { 43 | return funcErr(err) 44 | } 45 | if !impl { 46 | return nil 47 | } 48 | 49 | // Get the spec 50 | spec, err := c.Client.DestroySpec(context.Background(), &empty.Empty{}) 51 | if err != nil { 52 | return funcErr(err) 53 | } 54 | 55 | return funcspec.Func(spec, c.destroy, 56 | argmapper.Logger(c.Logger), 57 | argmapper.Typed(&pluginargs.Internal{ 58 | Broker: c.Broker, 59 | Mappers: c.Mappers, 60 | Cleanup: &pluginargs.Cleanup{}, 61 | }), 62 | ) 63 | } 64 | 65 | func (c *destroyerClient) destroy( 66 | ctx context.Context, 67 | args funcspec.Args, 68 | internal *pluginargs.Internal, 69 | declaredResourcesResp *component.DeclaredResourcesResp, 70 | destroyedResourcesResp *component.DestroyedResourcesResp, 71 | ) error { 72 | // Run the cleanup 73 | defer internal.Cleanup.Close() 74 | 75 | // Call our function 76 | resp, err := c.Client.Destroy(ctx, &pb.FuncSpec_Args{Args: args}) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | declaredResourcesResp.DeclaredResources = resp.DeclaredResources.Resources 82 | destroyedResourcesResp.DestroyedResources = resp.DestroyedResources.DestroyedResources 83 | 84 | return nil 85 | } 86 | 87 | // destroyerServer implements the common Destroyer-related RPC calls. 88 | // This should be embedded into the service implementation. 89 | type destroyerServer struct { 90 | *base 91 | Impl interface{} 92 | } 93 | 94 | func (s *destroyerServer) IsDestroyer( 95 | ctx context.Context, 96 | empty *empty.Empty, 97 | ) (*pb.ImplementsResp, error) { 98 | d, ok := s.Impl.(component.Destroyer) 99 | return &pb.ImplementsResp{ 100 | Implements: ok && d.DestroyFunc() != nil, 101 | }, nil 102 | } 103 | 104 | func (s *destroyerServer) DestroySpec( 105 | ctx context.Context, 106 | args *empty.Empty, 107 | ) (*pb.FuncSpec, error) { 108 | return funcspec.Spec(s.Impl.(component.Destroyer).DestroyFunc(), 109 | //argmapper.WithNoOutput(), // we only expect an error value so ignore the rest 110 | argmapper.ConverterFunc(s.Mappers...), 111 | argmapper.Logger(s.Logger), 112 | argmapper.Typed(s.internal()), 113 | ) 114 | } 115 | 116 | func (s *destroyerServer) Destroy( 117 | ctx context.Context, 118 | args *pb.FuncSpec_Args, 119 | ) (*pb.Destroy_Resp, error) { 120 | internal := s.internal() 121 | defer internal.Cleanup.Close() 122 | 123 | // Inject our outparameters, so we can capture the response after invocation 124 | declaredResourcesResp := &component.DeclaredResourcesResp{} 125 | destroyedResourcesResp := &component.DestroyedResourcesResp{} 126 | 127 | _, err := callDynamicFunc2(s.Impl.(component.Destroyer).DestroyFunc(), args.Args, 128 | argmapper.ConverterFunc(s.Mappers...), 129 | argmapper.Typed(internal), 130 | argmapper.Typed(ctx), 131 | argmapper.Typed(declaredResourcesResp), 132 | argmapper.Typed(destroyedResourcesResp), 133 | ) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | return &pb.Destroy_Resp{ 139 | DeclaredResources: &pb.DeclaredResources{ 140 | Resources: declaredResourcesResp.DeclaredResources, 141 | }, 142 | DestroyedResources: &pb.DestroyedResources{ 143 | DestroyedResources: destroyedResourcesResp.DestroyedResources, 144 | }, 145 | }, nil 146 | } 147 | 148 | // destroyerProtoClient is the interface we expect any gRPC service that 149 | // supports destroy to implement. 150 | type destroyerProtoClient interface { 151 | IsDestroyer(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.ImplementsResp, error) 152 | DestroySpec(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.FuncSpec, error) 153 | Destroy(context.Context, *pb.FuncSpec_Args, ...grpc.CallOption) (*pb.Destroy_Resp, error) 154 | } 155 | 156 | var ( 157 | _ component.Destroyer = (*destroyerClient)(nil) 158 | ) 159 | -------------------------------------------------------------------------------- /internal/plugin/destroyer_workspace.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/go-plugin" 9 | "google.golang.org/grpc" 10 | empty "google.golang.org/protobuf/types/known/emptypb" 11 | 12 | "github.com/hashicorp/waypoint-plugin-sdk/component" 13 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 14 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 15 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 16 | ) 17 | 18 | // workspaceDestroyerClient implements component.WorkspaceDestroyer for a service that 19 | // has the destroy methods implemented. 20 | type workspaceDestroyerClient struct { 21 | Client workspaceDestroyerProtoClient 22 | Logger hclog.Logger 23 | Broker *plugin.GRPCBroker 24 | Mappers []*argmapper.Func 25 | } 26 | 27 | func (c *workspaceDestroyerClient) Implements(ctx context.Context) (bool, error) { 28 | if c == nil { 29 | return false, nil 30 | } 31 | 32 | resp, err := c.Client.IsWorkspaceDestroyer(ctx, &empty.Empty{}) 33 | if err != nil { 34 | return false, err 35 | } 36 | 37 | return resp.Implements, nil 38 | } 39 | 40 | func (c *workspaceDestroyerClient) DestroyWorkspaceFunc() interface{} { 41 | impl, err := c.Implements(context.Background()) 42 | if err != nil { 43 | return funcErr(err) 44 | } 45 | if !impl { 46 | return nil 47 | } 48 | 49 | // Get the spec 50 | spec, err := c.Client.DestroyWorkspaceSpec(context.Background(), &empty.Empty{}) 51 | if err != nil { 52 | return funcErr(err) 53 | } 54 | 55 | return funcspec.Func(spec, c.destroy, 56 | argmapper.Logger(c.Logger), 57 | argmapper.Typed(&pluginargs.Internal{ 58 | Broker: c.Broker, 59 | Mappers: c.Mappers, 60 | Cleanup: &pluginargs.Cleanup{}, 61 | }), 62 | ) 63 | } 64 | 65 | func (c *workspaceDestroyerClient) destroy( 66 | ctx context.Context, 67 | args funcspec.Args, 68 | internal *pluginargs.Internal, 69 | ) error { 70 | // Run the cleanup 71 | defer internal.Cleanup.Close() 72 | 73 | // Call our function 74 | _, err := c.Client.DestroyWorkspace(ctx, &pb.FuncSpec_Args{Args: args}) 75 | return err 76 | } 77 | 78 | // workspaceDestroyerServer implements the common WorkspaceDestroyer-related RPC calls. 79 | // This should be embedded into the service implementation. 80 | type workspaceDestroyerServer struct { 81 | *base 82 | Impl interface{} 83 | } 84 | 85 | func (s *workspaceDestroyerServer) IsWorkspaceDestroyer( 86 | ctx context.Context, 87 | empty *empty.Empty, 88 | ) (*pb.ImplementsResp, error) { 89 | d, ok := s.Impl.(component.WorkspaceDestroyer) 90 | return &pb.ImplementsResp{ 91 | Implements: ok && d.DestroyWorkspaceFunc() != nil, 92 | }, nil 93 | } 94 | 95 | func (s *workspaceDestroyerServer) DestroyWorkspaceSpec( 96 | ctx context.Context, 97 | args *empty.Empty, 98 | ) (*pb.FuncSpec, error) { 99 | return funcspec.Spec(s.Impl.(component.WorkspaceDestroyer).DestroyWorkspaceFunc(), 100 | //argmapper.WithNoOutput(), // we only expect an error value so ignore the rest 101 | argmapper.ConverterFunc(s.Mappers...), 102 | argmapper.Logger(s.Logger), 103 | argmapper.Typed(s.internal()), 104 | ) 105 | } 106 | 107 | func (s *workspaceDestroyerServer) DestroyWorkspace( 108 | ctx context.Context, 109 | args *pb.FuncSpec_Args, 110 | ) (*empty.Empty, error) { 111 | internal := s.internal() 112 | defer internal.Cleanup.Close() 113 | 114 | _, err := callDynamicFunc2(s.Impl.(component.WorkspaceDestroyer).DestroyWorkspaceFunc(), args.Args, 115 | argmapper.ConverterFunc(s.Mappers...), 116 | argmapper.Typed(internal), 117 | argmapper.Typed(ctx), 118 | ) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return &empty.Empty{}, nil 124 | } 125 | 126 | // workspaceDestroyerProtoClient is the interface we expect any gRPC service that 127 | // supports destroy to implement. 128 | type workspaceDestroyerProtoClient interface { 129 | IsWorkspaceDestroyer(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.ImplementsResp, error) 130 | DestroyWorkspaceSpec(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.FuncSpec, error) 131 | DestroyWorkspace(context.Context, *pb.FuncSpec_Args, ...grpc.CallOption) (*empty.Empty, error) 132 | } 133 | 134 | var ( 135 | _ component.WorkspaceDestroyer = (*workspaceDestroyerClient)(nil) 136 | ) 137 | -------------------------------------------------------------------------------- /internal/plugin/dynamic_call.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/opaqueany" 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "google.golang.org/protobuf/proto" 11 | "google.golang.org/protobuf/reflect/protoregistry" 12 | 13 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 14 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 15 | ) 16 | 17 | // callDynamicFunc calls a dynamic (mapper-based) function with the 18 | // given input arguments. This is a helper that is expected to be used 19 | // by most component gRPC servers to implement their function calls. 20 | func callDynamicFunc2( 21 | f interface{}, 22 | args funcspec.Args, 23 | callArgs ...argmapper.Arg, 24 | ) (interface{}, error) { 25 | // Decode our *opaqueany.Any values. 26 | for _, arg := range args { 27 | var value interface{} 28 | var err error 29 | switch v := arg.Value.(type) { 30 | case *pb.FuncSpec_Value_ProtoAny: 31 | value, err = argProtoAny(arg) 32 | 33 | case *pb.FuncSpec_Value_Bool: 34 | value = v.Bool 35 | 36 | case *pb.FuncSpec_Value_Int: 37 | switch arg.PrimitiveType { 38 | case pb.FuncSpec_Value_INT8: 39 | value = int8(v.Int) 40 | case pb.FuncSpec_Value_INT16: 41 | value = int16(v.Int) 42 | case pb.FuncSpec_Value_INT32: 43 | value = int32(v.Int) 44 | case pb.FuncSpec_Value_INT64: 45 | value = int64(v.Int) 46 | case pb.FuncSpec_Value_INT: 47 | fallthrough 48 | default: 49 | // Fallback to int as a default 50 | value = int(v.Int) 51 | } 52 | 53 | case *pb.FuncSpec_Value_Uint: 54 | switch arg.PrimitiveType { 55 | case pb.FuncSpec_Value_INT8: 56 | value = uint8(v.Uint) 57 | case pb.FuncSpec_Value_INT16: 58 | value = uint16(v.Uint) 59 | case pb.FuncSpec_Value_INT32: 60 | value = uint32(v.Uint) 61 | case pb.FuncSpec_Value_INT64: 62 | value = uint64(v.Uint) 63 | case pb.FuncSpec_Value_INT: 64 | fallthrough 65 | default: 66 | // Fallback to uint as a default 67 | value = uint(v.Uint) 68 | } 69 | 70 | case *pb.FuncSpec_Value_String_: 71 | value = v.String_ 72 | 73 | default: 74 | return nil, fmt.Errorf("internal error! invalid argument value: %#v", 75 | arg.Value) 76 | } 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | callArgs = append(callArgs, 82 | argmapper.NamedSubtype(arg.Name, value, arg.Type), 83 | ) 84 | } 85 | 86 | mapF, err := argmapper.NewFunc(f) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | result := mapF.Call(callArgs...) 92 | if err := result.Err(); err != nil { 93 | return nil, err 94 | } 95 | 96 | return result.Out(0), nil 97 | } 98 | 99 | // callDynamicFuncAny is callDynamicFunc that automatically encodes the 100 | // result to an *opaqueany.Any. 101 | func callDynamicFuncAny2( 102 | f interface{}, 103 | args funcspec.Args, 104 | callArgs ...argmapper.Arg, 105 | ) (*opaqueany.Any, string, interface{}, error) { 106 | result, err := callDynamicFunc2(f, args, callArgs...) 107 | if err != nil { 108 | return nil, "", nil, err 109 | } 110 | 111 | // We expect the final result to always be a proto message so we can 112 | // send it back over the wire. 113 | // 114 | // NOTE(mitchellh): If we wanted to in the future, we can probably change 115 | // this to be any type that has a mapper that can take it to be a 116 | // proto.Message. 117 | msg, ok := result.(proto.Message) 118 | if !ok { 119 | return nil, "", nil, fmt.Errorf( 120 | "result of plugin-based function must be a proto.Message, got %T", msg) 121 | } 122 | 123 | anyVal, err := opaqueany.New(msg) 124 | if err != nil { 125 | return nil, "", nil, err 126 | } 127 | 128 | anyJson, err := protojson.Marshal(msg) 129 | if err != nil { 130 | return nil, "", nil, err 131 | } 132 | 133 | return anyVal, string(anyJson), result, err 134 | } 135 | 136 | func argProtoAny(arg *pb.FuncSpec_Value) (interface{}, error) { 137 | anyVal := arg.Value.(*pb.FuncSpec_Value_ProtoAny).ProtoAny 138 | 139 | name := anyVal.MessageName() 140 | 141 | mt, err := protoregistry.GlobalTypes.FindMessageByName(name) 142 | if err != nil { 143 | return nil, fmt.Errorf("cannot decode type: %s", name) 144 | } 145 | 146 | typ := reflect.TypeOf(proto.Message(mt.Zero().Interface())) 147 | 148 | // Allocate the message type. If it is a pointer we want to 149 | // allocate the actual structure and not the pointer to the structure. 150 | if typ.Kind() == reflect.Ptr { 151 | typ = typ.Elem() 152 | } 153 | v := reflect.New(typ) 154 | v.Elem().Set(reflect.Zero(typ)) 155 | 156 | // Unmarshal directly into our newly allocated structure. 157 | if err := anyVal.UnmarshalTo(v.Interface().(proto.Message)); err != nil { 158 | return nil, err 159 | } 160 | 161 | return v.Interface(), nil 162 | } 163 | -------------------------------------------------------------------------------- /internal/plugin/error.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // funcErr returns a function that can be returned for any of the 8 | // Func component calls that just returns an error. This lets us surface 9 | // RPC errors cleanly rather than a panic. 10 | func funcErr(err error) interface{} { 11 | return func(context.Context) (interface{}, error) { 12 | return nil, err 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/plugin/execer.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | empty "google.golang.org/protobuf/types/known/emptypb" 14 | 15 | "github.com/hashicorp/waypoint-plugin-sdk/component" 16 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 17 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 18 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 19 | ) 20 | 21 | // execerClient implements component.Execer for a service that 22 | // has the exec methods implemented. 23 | type execerClient struct { 24 | Client execerProtoClient 25 | Logger hclog.Logger 26 | Broker *plugin.GRPCBroker 27 | Mappers []*argmapper.Func 28 | } 29 | 30 | func (c *execerClient) Implements(ctx context.Context) (bool, error) { 31 | if c == nil { 32 | return false, nil 33 | } 34 | 35 | resp, err := c.Client.IsExecer(ctx, &empty.Empty{}) 36 | if err != nil { 37 | // If the plugin doesn't implement IsExecer the RPC, then it definitely doesn't 38 | // implement it. If we return err here, it will blow up the whole usage of this 39 | // type so just say "sorry, not implemented" so the core can continue to run. 40 | if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { 41 | return false, nil 42 | } 43 | return false, err 44 | } 45 | 46 | return resp.Implements, nil 47 | } 48 | 49 | func (c *execerClient) ExecFunc() interface{} { 50 | impl, err := c.Implements(context.Background()) 51 | if err != nil { 52 | return funcErr(err) 53 | } 54 | if !impl { 55 | return nil 56 | } 57 | 58 | // Get the spec 59 | spec, err := c.Client.ExecSpec(context.Background(), &empty.Empty{}) 60 | if err != nil { 61 | return funcErr(err) 62 | } 63 | 64 | return funcspec.Func(spec, c.exec, 65 | argmapper.Logger(c.Logger), 66 | argmapper.Typed(&pluginargs.Internal{ 67 | Broker: c.Broker, 68 | Mappers: c.Mappers, 69 | Cleanup: &pluginargs.Cleanup{}, 70 | }), 71 | ) 72 | } 73 | 74 | func (c *execerClient) exec( 75 | ctx context.Context, 76 | args funcspec.Args, 77 | internal *pluginargs.Internal, 78 | ) (*component.ExecResult, error) { 79 | // Run the cleanup 80 | defer internal.Cleanup.Close() 81 | 82 | // Call our function 83 | resp, err := c.Client.Exec(ctx, &pb.FuncSpec_Args{Args: args}) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return &component.ExecResult{ 89 | ExitCode: int(resp.ExitCode), 90 | }, nil 91 | } 92 | 93 | // execerServer implements the common Execer-related RPC calls. 94 | // This should be embedded into the service implementation. 95 | type execerServer struct { 96 | *base 97 | Impl interface{} 98 | } 99 | 100 | func (s *execerServer) IsExecer( 101 | ctx context.Context, 102 | empty *empty.Empty, 103 | ) (*pb.ImplementsResp, error) { 104 | d, ok := s.Impl.(component.Execer) 105 | return &pb.ImplementsResp{ 106 | Implements: ok && d.ExecFunc() != nil, 107 | }, nil 108 | } 109 | 110 | func (s *execerServer) ExecSpec( 111 | ctx context.Context, 112 | args *empty.Empty, 113 | ) (*pb.FuncSpec, error) { 114 | return funcspec.Spec(s.Impl.(component.Execer).ExecFunc(), 115 | //argmapper.WithNoOutput(), // we only expect an error value so ignore the rest 116 | argmapper.ConverterFunc(s.Mappers...), 117 | argmapper.Logger(s.Logger), 118 | argmapper.Typed(s.internal()), 119 | argmapper.FilterOutput( 120 | argmapper.FilterType(reflect.TypeOf((*component.ExecResult)(nil))), 121 | ), 122 | ) 123 | } 124 | 125 | func (s *execerServer) Exec( 126 | ctx context.Context, 127 | args *pb.FuncSpec_Args, 128 | ) (*pb.ExecResult, error) { 129 | internal := s.internal() 130 | defer internal.Cleanup.Close() 131 | 132 | result, err := callDynamicFunc2(s.Impl.(component.Execer).ExecFunc(), args.Args, 133 | argmapper.ConverterFunc(s.Mappers...), 134 | argmapper.Typed(internal), 135 | argmapper.Typed(ctx), 136 | ) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | ret := &pb.ExecResult{} 142 | 143 | if ec, ok := result.(*component.ExecResult); ok { 144 | ret.ExitCode = int32(ec.ExitCode) 145 | } 146 | 147 | return ret, nil 148 | } 149 | 150 | // execerProtoClient is the interface we expect any gRPC service that 151 | // supports exec to implement. 152 | type execerProtoClient interface { 153 | IsExecer(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.ImplementsResp, error) 154 | ExecSpec(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.FuncSpec, error) 155 | Exec(context.Context, *pb.FuncSpec_Args, ...grpc.CallOption) (*pb.ExecResult, error) 156 | } 157 | 158 | var ( 159 | _ component.Execer = (*execerClient)(nil) 160 | ) 161 | -------------------------------------------------------------------------------- /internal/plugin/generation.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | "google.golang.org/grpc" 11 | empty "google.golang.org/protobuf/types/known/emptypb" 12 | 13 | "github.com/hashicorp/waypoint-plugin-sdk/component" 14 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 15 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 16 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 17 | ) 18 | 19 | // generationClient implements component.Generation for a service that 20 | // has the generation ID methods implemented. 21 | type generationClient struct { 22 | Client generationProtoClient 23 | Logger hclog.Logger 24 | Broker *plugin.GRPCBroker 25 | Mappers []*argmapper.Func 26 | } 27 | 28 | func (c *generationClient) Implements(ctx context.Context) (bool, error) { 29 | if c == nil { 30 | return false, nil 31 | } 32 | 33 | resp, err := c.Client.IsGeneration(ctx, &empty.Empty{}) 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | return resp.Implements, nil 39 | } 40 | 41 | func (c *generationClient) GenerationFunc() interface{} { 42 | impl, err := c.Implements(context.Background()) 43 | if err != nil { 44 | return funcErr(err) 45 | } 46 | if !impl { 47 | return nil 48 | } 49 | 50 | // Get the spec 51 | spec, err := c.Client.GenerationSpec(context.Background(), &empty.Empty{}) 52 | if err != nil { 53 | return funcErr(err) 54 | } 55 | 56 | return funcspec.Func(spec, c.generation, 57 | argmapper.Logger(c.Logger), 58 | argmapper.Typed(&pluginargs.Internal{ 59 | Broker: c.Broker, 60 | Mappers: c.Mappers, 61 | Cleanup: &pluginargs.Cleanup{}, 62 | }), 63 | ) 64 | } 65 | 66 | func (c *generationClient) generation( 67 | ctx context.Context, 68 | args funcspec.Args, 69 | internal *pluginargs.Internal, 70 | ) ([]byte, error) { 71 | // Run the cleanup 72 | defer internal.Cleanup.Close() 73 | 74 | // Call our function 75 | resp, err := c.Client.Generation(ctx, &pb.FuncSpec_Args{Args: args}) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return resp.Id, nil 81 | } 82 | 83 | // generationServer implements the common Generation-related RPC calls. 84 | // This should be embedded into the service implementation. 85 | type generationServer struct { 86 | *base 87 | Impl interface{} 88 | } 89 | 90 | func (s *generationServer) IsGeneration( 91 | ctx context.Context, 92 | empty *empty.Empty, 93 | ) (*pb.ImplementsResp, error) { 94 | d, ok := s.Impl.(component.Generation) 95 | return &pb.ImplementsResp{ 96 | Implements: ok && d.GenerationFunc() != nil, 97 | }, nil 98 | } 99 | 100 | func (s *generationServer) GenerationSpec( 101 | ctx context.Context, 102 | args *empty.Empty, 103 | ) (*pb.FuncSpec, error) { 104 | return funcspec.Spec(s.Impl.(component.Generation).GenerationFunc(), 105 | argmapper.ConverterFunc(s.Mappers...), 106 | argmapper.Logger(s.Logger), 107 | argmapper.Typed(s.internal()), 108 | argmapper.FilterOutput( 109 | argmapper.FilterType(reflect.TypeOf([]byte(nil))), 110 | ), 111 | ) 112 | } 113 | 114 | func (s *generationServer) Generation( 115 | ctx context.Context, 116 | args *pb.FuncSpec_Args, 117 | ) (*pb.Generation_Resp, error) { 118 | internal := s.internal() 119 | defer internal.Cleanup.Close() 120 | 121 | resp, err := callDynamicFunc2(s.Impl.(component.Generation).GenerationFunc(), args.Args, 122 | argmapper.ConverterFunc(s.Mappers...), 123 | argmapper.Typed(internal), 124 | argmapper.Typed(ctx), 125 | ) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return &pb.Generation_Resp{ 131 | Id: resp.([]byte), 132 | }, nil 133 | } 134 | 135 | // generationProtoClient is the interface we expect any gRPC service that 136 | // supports component.Generation to implement. 137 | type generationProtoClient interface { 138 | IsGeneration(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.ImplementsResp, error) 139 | GenerationSpec(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.FuncSpec, error) 140 | Generation(context.Context, *pb.FuncSpec_Args, ...grpc.CallOption) (*pb.Generation_Resp, error) 141 | } 142 | 143 | var ( 144 | _ component.Generation = (*generationClient)(nil) 145 | ) 146 | -------------------------------------------------------------------------------- /internal/plugin/logs.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/hashicorp/waypoint-plugin-sdk/component" 10 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 11 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 12 | proto "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 13 | empty "google.golang.org/protobuf/types/known/emptypb" 14 | ) 15 | 16 | // logClient is an implementation of component.LogPlatform over gRPC. 17 | type logClient struct { 18 | Client proto.PlatformClient 19 | Logger hclog.Logger 20 | Broker *plugin.GRPCBroker 21 | Mappers []*argmapper.Func 22 | } 23 | 24 | func (c *logClient) Implements(ctx context.Context) (bool, error) { 25 | if c == nil { 26 | return false, nil 27 | } 28 | 29 | resp, err := c.Client.IsLogPlatform(ctx, &empty.Empty{}) 30 | if err != nil { 31 | return false, err 32 | } 33 | 34 | return resp.Implements, nil 35 | } 36 | 37 | func (c *logClient) LogsFunc() interface{} { 38 | // If we're not initialized, then it doesn't support LogsFunc 39 | if c == nil { 40 | return nil 41 | } 42 | 43 | // Get the spec 44 | spec, err := c.Client.LogsSpec(context.Background(), &empty.Empty{}) 45 | if err != nil { 46 | return funcErr(err) 47 | } 48 | 49 | // We don't want to be a mapper 50 | spec.Result = nil 51 | 52 | return funcspec.Func(spec, c.logs, 53 | argmapper.Logger(c.Logger), 54 | argmapper.Typed(&pluginargs.Internal{ 55 | Broker: c.Broker, 56 | Mappers: c.Mappers, 57 | Cleanup: &pluginargs.Cleanup{}, 58 | }), 59 | ) 60 | } 61 | 62 | func (c *logClient) logs( 63 | ctx context.Context, 64 | args funcspec.Args, 65 | internal *pluginargs.Internal, 66 | ) error { 67 | // Run the cleanup 68 | defer internal.Cleanup.Close() 69 | 70 | // Call our function 71 | _, err := c.Client.Logs(ctx, &proto.FuncSpec_Args{Args: args}) 72 | return err 73 | } 74 | 75 | // logPlatformServer is a gRPC server that the client talks to and calls a 76 | // real implementation of the component. 77 | type logPlatformServer struct { 78 | *base 79 | 80 | Impl interface{} 81 | } 82 | 83 | func (s *logPlatformServer) IsLogPlatform( 84 | ctx context.Context, 85 | empty *empty.Empty, 86 | ) (*proto.ImplementsResp, error) { 87 | d, ok := s.Impl.(component.LogPlatform) 88 | return &proto.ImplementsResp{ 89 | Implements: ok && d.LogsFunc() != nil, 90 | }, nil 91 | } 92 | 93 | func (s *logPlatformServer) LogsSpec( 94 | ctx context.Context, 95 | args *empty.Empty, 96 | ) (*proto.FuncSpec, error) { 97 | return funcspec.Spec(s.Impl.(component.LogPlatform).LogsFunc(), 98 | argmapper.ConverterFunc(s.Mappers...), 99 | argmapper.Logger(s.Logger), 100 | argmapper.Typed(s.internal()), 101 | ) 102 | } 103 | 104 | func (s *logPlatformServer) Logs( 105 | ctx context.Context, 106 | args *proto.FuncSpec_Args, 107 | ) (*empty.Empty, error) { 108 | internal := s.internal() 109 | defer internal.Cleanup.Close() 110 | 111 | _, err := callDynamicFunc2(s.Impl.(component.LogPlatform).LogsFunc(), args.Args, 112 | argmapper.Typed(ctx), 113 | argmapper.ConverterFunc(s.Mappers...), 114 | argmapper.Typed(internal), 115 | ) 116 | return &empty.Empty{}, err 117 | } 118 | 119 | var ( 120 | _ component.LogPlatform = (*logClient)(nil) 121 | ) 122 | -------------------------------------------------------------------------------- /internal/plugin/logs/logs.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-argmapper" 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/hashicorp/go-plugin" 9 | "google.golang.org/grpc" 10 | "google.golang.org/protobuf/types/known/timestamppb" 11 | 12 | "github.com/hashicorp/waypoint-plugin-sdk/component" 13 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 14 | ) 15 | 16 | // UIPlugin implements plugin.Plugin (specifically GRPCPlugin) for 17 | // the terminal.UI interface. 18 | type LogsPlugin struct { 19 | plugin.NetRPCUnsupportedPlugin 20 | 21 | Impl *component.LogViewer // Impl is the concrete implementation 22 | Mappers []*argmapper.Func // Mappers 23 | Logger hclog.Logger // Logger 24 | } 25 | 26 | func (p *LogsPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 27 | pb.RegisterLogViewerServer(s, &logsServer{ 28 | Impl: p.Impl, 29 | Mappers: p.Mappers, 30 | Logger: p.Logger, 31 | }) 32 | return nil 33 | } 34 | 35 | func (p *LogsPlugin) GRPCClient( 36 | ctx context.Context, 37 | broker *plugin.GRPCBroker, 38 | c *grpc.ClientConn, 39 | ) (interface{}, error) { 40 | p.Logger.Debug("starting logviewer client") 41 | 42 | client := pb.NewLogViewerClient(c) 43 | 44 | nlb, err := client.NextLogBatch(ctx) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | // TODO(evanphx) figure out the right backlog for this 50 | output := make(chan component.LogEvent, 10) 51 | 52 | /* 53 | go func() { 54 | for { 55 | req, err := stream.Recv() 56 | if err != nil { 57 | p.Logger.Debug("exec plugin input stream exitted", "error", err) 58 | return 59 | } 60 | 61 | p.Logger.Debug("processing logviewer events", "events", len(req.Events)) 62 | 63 | for _, ev := range req.Events { 64 | 65 | out := component.LogEvent{ 66 | Partition: ev.Partition, 67 | Timestamp: ev.Timestamp.AsTime(), 68 | Message: ev.Contents, 69 | } 70 | select { 71 | case <-ctx.Done(): 72 | return 73 | case output <- out: 74 | // ok 75 | } 76 | } 77 | } 78 | }() 79 | */ 80 | 81 | go func() { 82 | for { 83 | select { 84 | case <-ctx.Done(): 85 | return 86 | case cle, ok := <-output: 87 | if !ok { 88 | return 89 | } 90 | 91 | nlb.Send(&pb.Logs_NextBatchResp{ 92 | Events: []*pb.Logs_Event{ 93 | { 94 | Partition: cle.Partition, 95 | Timestamp: timestamppb.New(cle.Timestamp), 96 | Contents: cle.Message, 97 | }, 98 | }, 99 | }) 100 | } 101 | } 102 | }() 103 | 104 | lv := &component.LogViewer{ 105 | Output: output, 106 | } 107 | 108 | return lv, nil 109 | } 110 | 111 | // logsServer is a gRPC server that the client talks to and calls a 112 | // real implementation of the component. 113 | type logsServer struct { 114 | pb.UnimplementedLogViewerServer 115 | 116 | Impl *component.LogViewer 117 | Mappers []*argmapper.Func 118 | Logger hclog.Logger 119 | } 120 | 121 | func (s *logsServer) NextLogBatch(lv pb.LogViewer_NextLogBatchServer) error { 122 | s.Logger.Debug("starting nextlogbatch rpc") 123 | defer s.Logger.Debug("ending nextlogbatch rpc") 124 | 125 | for { 126 | chunk, err := lv.Recv() 127 | if err != nil { 128 | return err 129 | } 130 | 131 | for _, ev := range chunk.Events { 132 | out := component.LogEvent{ 133 | Partition: ev.Partition, 134 | Timestamp: ev.Timestamp.AsTime(), 135 | Message: ev.Contents, 136 | } 137 | select { 138 | case <-lv.Context().Done(): 139 | return nil 140 | case s.Impl.Output <- out: 141 | // ok 142 | } 143 | } 144 | } 145 | 146 | /* 147 | for { 148 | select { 149 | case <-lv.Context().Done(): 150 | return lv.Context().Err() 151 | case log, ok := <-s.Impl.Output: 152 | if !ok { 153 | return io.EOF 154 | } 155 | 156 | s.Logger.Debug("sending log event") 157 | 158 | lv.Send(&pb.Logs_NextBatchResp{ 159 | Events: []*pb.Logs_Event{ 160 | { 161 | Partition: log.Partition, 162 | Timestamp: timestamppb.New(log.Timestamp), 163 | Contents: log.Message, 164 | }, 165 | }, 166 | }) 167 | } 168 | } 169 | */ 170 | } 171 | 172 | var ( 173 | _ plugin.Plugin = (*LogsPlugin)(nil) 174 | _ plugin.GRPCPlugin = (*LogsPlugin)(nil) 175 | _ pb.LogViewerServer = (*logsServer)(nil) 176 | ) 177 | -------------------------------------------------------------------------------- /internal/plugin/mapper_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 13 | "github.com/hashicorp/waypoint-plugin-sdk/internal/testproto" 14 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 15 | ) 16 | 17 | func TestMapperClient(t *testing.T) { 18 | require := require.New(t) 19 | assert := assert.New(t) 20 | 21 | mA, err := argmapper.NewFunc(func(a *testproto.A) *testproto.B { 22 | return &testproto.B{Value: a.Value + 1} 23 | }) 24 | require.NoError(err) 25 | 26 | plugins := Plugins(WithMappers(append(testDefaultMappers(t), mA)...)) 27 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 28 | defer client.Close() 29 | defer server.Stop() 30 | 31 | raw, err := client.Dispense("mapper") 32 | require.NoError(err) 33 | mapper := raw.(*MapperClient) 34 | 35 | mappers, err := mapper.Mappers() 36 | require.NoError(err) 37 | require.NotEmpty(mappers) 38 | 39 | targetSpec := &pb.FuncSpec{ 40 | Args: []*pb.FuncSpec_Value{ 41 | { 42 | Type: "testproto.B", 43 | }, 44 | }, 45 | 46 | Result: []*pb.FuncSpec_Value{ 47 | { 48 | Type: "testproto.Data", 49 | }, 50 | }, 51 | } 52 | 53 | called := false 54 | target := funcspec.Func(targetSpec, func(args funcspec.Args) (interface{}, error) { 55 | cb := func(v *testproto.B) *testproto.Data { 56 | called = true 57 | assert.Equal(int32(2), v.Value) 58 | return &testproto.Data{} 59 | } 60 | 61 | return callDynamicFunc2(cb, args, 62 | argmapper.Typed(context.Background()), 63 | argmapper.ConverterFunc(mappers...), 64 | ) 65 | }) 66 | 67 | result := target.Call( 68 | argmapper.Typed(context.Background()), 69 | argmapper.Typed(&testproto.A{Value: 1}), 70 | argmapper.ConverterFunc(mappers...), 71 | ) 72 | require.NoError(result.Err()) 73 | require.True(called) 74 | } 75 | -------------------------------------------------------------------------------- /internal/plugin/platform_mix.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/waypoint-plugin-sdk/component" 5 | ) 6 | 7 | type mix_Platform_Authenticator struct { 8 | component.Authenticator 9 | component.ConfigurableNotify 10 | component.Documented 11 | component.Platform 12 | component.PlatformReleaser 13 | component.WorkspaceDestroyer 14 | component.LogPlatform 15 | component.Generation 16 | component.Status 17 | } 18 | 19 | type mix_Platform_Destroy struct { 20 | component.Authenticator 21 | component.ConfigurableNotify 22 | component.Documented 23 | component.Platform 24 | component.PlatformReleaser 25 | component.Execer 26 | component.LogPlatform 27 | component.Destroyer 28 | component.WorkspaceDestroyer 29 | component.Generation 30 | component.Status 31 | } 32 | 33 | type mix_Platform_Exec struct { 34 | component.Authenticator 35 | component.ConfigurableNotify 36 | component.Documented 37 | component.Platform 38 | component.PlatformReleaser 39 | component.LogPlatform 40 | component.Execer 41 | component.Generation 42 | component.Status 43 | } 44 | -------------------------------------------------------------------------------- /internal/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | ) 11 | 12 | // Handshake is a common handshake that is shared by plugin and host. 13 | var Handshake = plugin.HandshakeConfig{ 14 | // Not secret, just to avoid plugins being launched manually. The 15 | // cookie value is a random SHA256 via /dev/urandom. This cookie value 16 | // must NEVER be changed or plugins will stop working. 17 | MagicCookieKey: "WAYPOINT_PLUGIN", 18 | MagicCookieValue: "be6c1928786a4df0222c13eef44ac846da2c0d461d99addc93f804601c6b7205", 19 | } 20 | 21 | // Plugins returns the list of available plugins and initializes them with 22 | // the given components. This will panic if an invalid component is given. 23 | func Plugins(opts ...Option) map[int]plugin.PluginSet { 24 | var c pluginConfig 25 | for _, opt := range opts { 26 | opt(&c) 27 | } 28 | 29 | // If we have no logger, we use the default 30 | if c.Logger == nil { 31 | c.Logger = hclog.L() 32 | } 33 | 34 | // Build our plugin types 35 | result := map[int]plugin.PluginSet{ 36 | 1: { 37 | "mapper": &MapperPlugin{}, 38 | "builder": &BuilderPlugin{}, 39 | "platform": &PlatformPlugin{}, 40 | "registry": &RegistryPlugin{}, 41 | "releasemanager": &ReleaseManagerPlugin{}, 42 | "configsourcer": &ConfigSourcerPlugin{}, 43 | "tasklauncher": &TaskLauncherPlugin{}, 44 | }, 45 | } 46 | 47 | // Set the various field values 48 | for _, c := range c.Components { 49 | if err := setFieldValue(result, c); err != nil { 50 | panic(err) 51 | } 52 | } 53 | 54 | // Set the mappers 55 | if err := setFieldValue(result, c.Mappers); err != nil { 56 | panic(err) 57 | } 58 | // Set the logger 59 | if err := setFieldValue(result, c.Logger); err != nil { 60 | panic(err) 61 | } 62 | // Set the ODR settings 63 | if err := setFieldValue(result, c.ODR); err != nil { 64 | panic(err) 65 | } 66 | 67 | return result 68 | } 69 | 70 | // pluginConfig is used to configure Plugins via Option calls. 71 | type pluginConfig struct { 72 | Components []interface{} 73 | Mappers []*argmapper.Func 74 | Logger hclog.Logger 75 | ODR *ODRSetting 76 | } 77 | 78 | // Option configures Plugins 79 | type Option func(*pluginConfig) 80 | 81 | // WithComponents sets the components to configure for the plugins. 82 | // This will append to the components. 83 | func WithComponents(cs ...interface{}) Option { 84 | return func(c *pluginConfig) { c.Components = append(c.Components, cs...) } 85 | } 86 | 87 | // WithMappers sets the mappers to configure for the plugins. This will 88 | // append to the existing mappers. 89 | func WithMappers(ms ...*argmapper.Func) Option { 90 | return func(c *pluginConfig) { 91 | c.Mappers = append(c.Mappers, ms...) 92 | } 93 | } 94 | 95 | // WithLogger sets the logger for the plugins. 96 | func WithLogger(log hclog.Logger) Option { 97 | return func(c *pluginConfig) { c.Logger = log } 98 | } 99 | 100 | // ODRSetting are any specific settings associated with running a plugin 101 | // in Ondemand Runner (aka ODR) mode. 102 | type ODRSetting struct { 103 | Enabled bool 104 | } 105 | 106 | // WithODR sets the ODRSettings for the plugins that are created. 107 | func WithODR(odr *ODRSetting) Option { 108 | return func(c *pluginConfig) { c.ODR = odr } 109 | } 110 | 111 | // setFieldValue sets the given value c on any exported field of an available 112 | // plugin that matches the type of c. An error is returned if c can't be 113 | // assigned to ANY plugin type. 114 | // 115 | // preconditions: 116 | // - plugins in m are pointers to structs 117 | func setFieldValue(m map[int]plugin.PluginSet, c interface{}) error { 118 | cv := reflect.ValueOf(c) 119 | ct := cv.Type() 120 | 121 | // Go through each pluginset 122 | once := false 123 | for _, set := range m { 124 | // Go through each plugin 125 | for _, p := range set { 126 | // Get the value, dereferencing the pointer. We expect 127 | // the value to be &SomeStruct{} so we must deref once. 128 | v := reflect.ValueOf(p).Elem() 129 | 130 | // Go through all the fields 131 | for i := 0; i < v.NumField(); i++ { 132 | f := v.Field(i) 133 | 134 | // If the field is valid and our component can be assigned 135 | // to it then we set the value directly. We continue setting 136 | // values because some values we set are available in multiple 137 | // plugins (loggers for example). 138 | if f.IsValid() && ct.AssignableTo(f.Type()) { 139 | f.Set(cv) 140 | once = true 141 | } 142 | } 143 | } 144 | } 145 | 146 | if !once { 147 | return fmt.Errorf("no plugin available for setting field of type %T", c) 148 | } 149 | 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /internal/plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/hashicorp/waypoint-plugin-sdk/component" 14 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 15 | "github.com/hashicorp/waypoint-plugin-sdk/internal-shared/protomappers" 16 | "github.com/hashicorp/waypoint-plugin-sdk/internal/testproto" 17 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 18 | ) 19 | 20 | func init() { 21 | // Set our default log level lower for tests 22 | hclog.L().SetLevel(hclog.Debug) 23 | } 24 | 25 | func TestPlugins(t *testing.T) { 26 | require := require.New(t) 27 | 28 | mock := &mocks.Builder{} 29 | plugins := Plugins(WithComponents(mock)) 30 | bp := plugins[1]["builder"].(*BuilderPlugin) 31 | require.Equal(bp.Impl, mock) 32 | } 33 | 34 | func testDefaultMappers(t *testing.T) []*argmapper.Func { 35 | var mappers []*argmapper.Func 36 | for _, raw := range protomappers.All { 37 | f, err := argmapper.NewFunc(raw) 38 | require.NoError(t, err) 39 | mappers = append(mappers, f) 40 | } 41 | 42 | return mappers 43 | } 44 | 45 | // testDynamicFunc ensures that the dynamic function capabilities work 46 | // properly. This should be called for each individual dynamic function 47 | // the component exposes. 48 | func testDynamicFunc( 49 | t *testing.T, 50 | typ string, 51 | value interface{}, 52 | setFunc func(interface{}, interface{}), // set the function on your mock 53 | getFunc func(interface{}) interface{}, // get the function 54 | ) { 55 | require := require.New(t) 56 | assert := assert.New(t) 57 | 58 | // Our callback that we verify. We specify a LOT of args here because 59 | // we want to verify that each one will work properly. This is the core 60 | // of this test. 61 | called := false 62 | setFunc(value, func( 63 | ctx context.Context, 64 | args *component.Source, 65 | ) *testproto.Data { 66 | called = true 67 | assert.NotNil(ctx) 68 | assert.Equal("foo", args.App) 69 | 70 | return &testproto.Data{Value: "hello"} 71 | }) 72 | 73 | // Get the mappers 74 | mappers := testDefaultMappers(t) 75 | 76 | // Init the plugin server 77 | plugins := Plugins(WithComponents(value), WithMappers(mappers...)) 78 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 79 | defer client.Close() 80 | defer server.Stop() 81 | 82 | // Dispense the plugin 83 | raw, err := client.Dispense(typ) 84 | require.NoError(err) 85 | implFunc := getFunc(raw).(*argmapper.Func) 86 | 87 | // Call our function by building a chain. We use the chain so we 88 | // have access to the same level of mappers that a default plugin 89 | // would normally have. 90 | result := implFunc.Call( 91 | argmapper.ConverterFunc(mappers...), 92 | 93 | argmapper.Typed(context.Background()), 94 | argmapper.Typed(hclog.L()), 95 | 96 | argmapper.Typed(&pb.Args_Source{App: "foo"}), 97 | 98 | argmapper.Typed(&component.DeclaredResourcesResp{}), 99 | 100 | argmapper.Typed(&component.DestroyedResourcesResp{}), 101 | ) 102 | require.NoError(result.Err()) 103 | 104 | // We only require a result if the function type expects us to return 105 | // a result. Otherwise, we just expect nil because it is error-only. 106 | if result.Len() > 0 { 107 | require.NotNil(result.Out(0)) 108 | } 109 | 110 | require.True(called) 111 | } 112 | -------------------------------------------------------------------------------- /internal/plugin/registry_mix.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/waypoint-plugin-sdk/component" 5 | ) 6 | 7 | type mix_Registry_Authenticator struct { 8 | component.Authenticator 9 | component.ConfigurableNotify 10 | component.Registry 11 | component.Documented 12 | component.RegistryAccess 13 | } 14 | -------------------------------------------------------------------------------- /internal/plugin/registry_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/waypoint-plugin-sdk/component" 7 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 8 | ) 9 | 10 | func TestRegistryDynamicFunc_validateAuth(t *testing.T) { 11 | testDynamicFunc(t, "registry", &mockRegistryAuthenticator{}, func(v, f interface{}) { 12 | v.(*mockRegistryAuthenticator).Authenticator.On("ValidateAuthFunc").Return(f) 13 | }, func(raw interface{}) interface{} { 14 | return raw.(component.Authenticator).ValidateAuthFunc() 15 | }) 16 | } 17 | func TestRegistryDynamicFunc_auth(t *testing.T) { 18 | testDynamicFunc(t, "registry", &mockRegistryAuthenticator{}, func(v, f interface{}) { 19 | v.(*mockRegistryAuthenticator).Authenticator.On("AuthFunc").Return(f) 20 | }, func(raw interface{}) interface{} { 21 | return raw.(component.Authenticator).AuthFunc() 22 | }) 23 | } 24 | 25 | func TestRegistryConfig(t *testing.T) { 26 | mockV := &mockRegistryConfigurable{} 27 | testConfigurable(t, "registry", mockV, &mockV.Configurable) 28 | } 29 | 30 | type mockRegistryAuthenticator struct { 31 | mocks.Registry 32 | mocks.Authenticator 33 | } 34 | 35 | type mockRegistryConfigurable struct { 36 | mocks.Registry 37 | mocks.Configurable 38 | } 39 | -------------------------------------------------------------------------------- /internal/plugin/releaser_mix.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/waypoint-plugin-sdk/component" 5 | ) 6 | 7 | type mix_ReleaseManager_Authenticator struct { 8 | component.Authenticator 9 | component.ConfigurableNotify 10 | component.ReleaseManager 11 | component.Destroyer 12 | component.WorkspaceDestroyer 13 | component.Documented 14 | component.Status 15 | } 16 | -------------------------------------------------------------------------------- /internal/plugin/releaser_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/waypoint-plugin-sdk/component" 7 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 8 | ) 9 | 10 | func TestReleaseManagerDynamicFunc_validateAuth(t *testing.T) { 11 | testDynamicFunc(t, "releasemanager", &mockReleaseManagerAuthenticator{}, func(v, f interface{}) { 12 | v.(*mockReleaseManagerAuthenticator).Authenticator.On("ValidateAuthFunc").Return(f) 13 | }, func(raw interface{}) interface{} { 14 | return raw.(component.Authenticator).ValidateAuthFunc() 15 | }) 16 | } 17 | func TestReleaseManagerDynamicFunc_auth(t *testing.T) { 18 | testDynamicFunc(t, "releasemanager", &mockReleaseManagerAuthenticator{}, func(v, f interface{}) { 19 | v.(*mockReleaseManagerAuthenticator).Authenticator.On("AuthFunc").Return(f) 20 | }, func(raw interface{}) interface{} { 21 | return raw.(component.Authenticator).AuthFunc() 22 | }) 23 | } 24 | 25 | func TestReleaseManagerDynamicFunc_destroy(t *testing.T) { 26 | testDynamicFunc(t, "releasemanager", &mockReleaseManagerDestroyer{}, func(v, f interface{}) { 27 | v.(*mockReleaseManagerDestroyer).Destroyer.On("DestroyFunc").Return(f) 28 | }, func(raw interface{}) interface{} { 29 | return raw.(component.Destroyer).DestroyFunc() 30 | }) 31 | } 32 | 33 | func TestReleaseManagerConfig(t *testing.T) { 34 | mockV := &mockReleaseManagerConfigurable{} 35 | testConfigurable(t, "releasemanager", mockV, &mockV.Configurable) 36 | } 37 | 38 | type mockReleaseManagerAuthenticator struct { 39 | mocks.ReleaseManager 40 | mocks.Authenticator 41 | } 42 | 43 | type mockReleaseManagerConfigurable struct { 44 | mocks.ReleaseManager 45 | mocks.Configurable 46 | } 47 | 48 | type mockReleaseManagerDestroyer struct { 49 | mocks.ReleaseManager 50 | mocks.Destroyer 51 | } 52 | -------------------------------------------------------------------------------- /internal/plugin/status.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/hashicorp/go-argmapper" 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | empty "google.golang.org/protobuf/types/known/emptypb" 14 | 15 | "github.com/hashicorp/waypoint-plugin-sdk/component" 16 | "github.com/hashicorp/waypoint-plugin-sdk/internal/funcspec" 17 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pluginargs" 18 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 19 | ) 20 | 21 | // statusClient implements component.Status for a service that 22 | // has the status methods implemented. 23 | type statusClient struct { 24 | Client statusProtoClient 25 | Logger hclog.Logger 26 | Broker *plugin.GRPCBroker 27 | Mappers []*argmapper.Func 28 | } 29 | 30 | func (c *statusClient) Implements(ctx context.Context) (bool, error) { 31 | if c == nil { 32 | return false, nil 33 | } 34 | 35 | resp, err := c.Client.IsStatus(ctx, &empty.Empty{}) 36 | if err != nil { 37 | return false, err 38 | } 39 | 40 | return resp.Implements, nil 41 | } 42 | 43 | func (c *statusClient) StatusFunc() interface{} { 44 | impl, err := c.Implements(context.Background()) 45 | if err != nil { 46 | return funcErr(err) 47 | } 48 | if !impl { 49 | return nil 50 | } 51 | 52 | // Get the spec 53 | spec, err := c.Client.StatusSpec(context.Background(), &empty.Empty{}) 54 | if err != nil { 55 | return funcErr(err) 56 | } 57 | 58 | spec.Result = nil 59 | 60 | return funcspec.Func(spec, c.status, 61 | argmapper.Logger(c.Logger), 62 | argmapper.Typed(&pluginargs.Internal{ 63 | Broker: c.Broker, 64 | Mappers: c.Mappers, 65 | Cleanup: &pluginargs.Cleanup{}, 66 | }), 67 | ) 68 | } 69 | 70 | func (c *statusClient) status( 71 | ctx context.Context, 72 | args funcspec.Args, 73 | internal *pluginargs.Internal, 74 | ) (*pb.StatusReport, error) { 75 | // Run the cleanup 76 | defer internal.Cleanup.Close() 77 | 78 | // Call our function 79 | resp, err := c.Client.Status(ctx, &pb.FuncSpec_Args{Args: args}) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | // just return `resp` here, pass it along 85 | return resp, nil 86 | } 87 | 88 | // statusServer implements the common Status-related RPC calls. 89 | // This should be embedded into the service implementation. 90 | type statusServer struct { 91 | *base 92 | Impl interface{} 93 | } 94 | 95 | func (s *statusServer) IsStatus( 96 | ctx context.Context, 97 | empty *empty.Empty, 98 | ) (*pb.ImplementsResp, error) { 99 | d, ok := s.Impl.(component.Status) 100 | return &pb.ImplementsResp{ 101 | Implements: ok && d.StatusFunc() != nil, 102 | }, nil 103 | } 104 | 105 | func (s *statusServer) StatusSpec( 106 | ctx context.Context, 107 | args *empty.Empty, 108 | ) (*pb.FuncSpec, error) { 109 | return funcspec.Spec(s.Impl.(component.Status).StatusFunc(), 110 | //argmapper.WithNoOutput(), // we only expect an error value so ignore the rest 111 | argmapper.ConverterFunc(s.Mappers...), 112 | argmapper.Logger(s.Logger), 113 | argmapper.Typed(s.internal()), 114 | 115 | argmapper.FilterOutput(argmapper.FilterType( 116 | reflect.TypeOf((*pb.StatusReport)(nil))), 117 | ), 118 | ) 119 | } 120 | 121 | func (s *statusServer) Status( 122 | ctx context.Context, 123 | args *pb.FuncSpec_Args, 124 | ) (*pb.StatusReport, error) { 125 | internal := s.internal() 126 | defer internal.Cleanup.Close() 127 | 128 | raw, err := callDynamicFunc2(s.Impl.(component.Status).StatusFunc(), args.Args, 129 | argmapper.ConverterFunc(s.Mappers...), 130 | argmapper.Typed(internal), 131 | argmapper.Typed(ctx), 132 | ) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | result, ok := raw.(*pb.StatusReport) 138 | if !ok { 139 | return nil, status.Errorf(codes.Aborted, "status result is not a *proto.StatusReport") 140 | } 141 | 142 | // validation could happen here 143 | // just return the proto instead 144 | return result, nil 145 | } 146 | 147 | // statusProtoClient is the interface we expect any gRPC service that 148 | // supports status to implement. 149 | type statusProtoClient interface { 150 | IsStatus(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.ImplementsResp, error) 151 | StatusSpec(context.Context, *empty.Empty, ...grpc.CallOption) (*pb.FuncSpec, error) 152 | Status(context.Context, *pb.FuncSpec_Args, ...grpc.CallOption) (*pb.StatusReport, error) 153 | } 154 | 155 | var ( 156 | _ component.Status = (*statusClient)(nil) 157 | ) 158 | -------------------------------------------------------------------------------- /internal/plugin/task_mix.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/waypoint-plugin-sdk/component" 5 | ) 6 | 7 | type mix_TaskLauncher_Authenticator struct { 8 | component.ConfigurableNotify 9 | component.TaskLauncher 10 | component.Documented 11 | } 12 | -------------------------------------------------------------------------------- /internal/plugin/task_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/hashicorp/go-argmapper" 9 | "github.com/hashicorp/go-plugin" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/hashicorp/waypoint-plugin-sdk/component" 14 | "github.com/hashicorp/waypoint-plugin-sdk/component/mocks" 15 | "github.com/hashicorp/waypoint-plugin-sdk/internal/testproto" 16 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 17 | ) 18 | 19 | func TestTaskLauncherStart(t *testing.T) { 20 | require := require.New(t) 21 | assert := assert.New(t) 22 | 23 | called := false 24 | startFunc := func(ctx context.Context, args *component.Source) *testproto.Data { 25 | called = true 26 | assert.NotNil(ctx) 27 | assert.Equal("foo", args.App) 28 | return &testproto.Data{Value: "hello"} 29 | } 30 | 31 | mockB := &mocks.TaskLauncher{} 32 | mockB.On("StartTaskFunc").Return(startFunc) 33 | mockB.On("StopTaskFunc").Return(startFunc) 34 | 35 | plugins := Plugins(WithComponents(mockB), WithMappers(testDefaultMappers(t)...)) 36 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 37 | defer client.Close() 38 | defer server.Stop() 39 | 40 | raw, err := client.Dispense("tasklauncher") 41 | require.NoError(err) 42 | fmt.Printf("=> %T\n", raw) 43 | bt := raw.(component.TaskLauncher) 44 | f := bt.StartTaskFunc().(*argmapper.Func) 45 | require.NotNil(f) 46 | 47 | result := f.Call( 48 | argmapper.Typed(context.Background()), 49 | argmapper.Typed(&pb.Args_Source{App: "foo"}), 50 | ) 51 | require.NoError(result.Err()) 52 | 53 | raw = result.Out(0) 54 | require.NotNil(raw) 55 | 56 | _, ok := raw.(component.RunningTask) 57 | require.True(ok) 58 | 59 | require.True(called) 60 | } 61 | 62 | func TestTaskLauncherWatch(t *testing.T) { 63 | require := require.New(t) 64 | assert := assert.New(t) 65 | 66 | called := false 67 | watchFunc := func(ctx context.Context, args *component.Source) (*component.TaskResult, error) { 68 | called = true 69 | assert.NotNil(ctx) 70 | assert.Equal("foo", args.App) 71 | return &component.TaskResult{ExitCode: 42}, nil 72 | } 73 | 74 | mockB := &mocks.TaskLauncher{} 75 | mockB.On("WatchTaskFunc").Return(watchFunc) 76 | 77 | plugins := Plugins(WithComponents(mockB), WithMappers(testDefaultMappers(t)...)) 78 | client, server := plugin.TestPluginGRPCConn(t, plugins[1]) 79 | defer client.Close() 80 | defer server.Stop() 81 | 82 | raw, err := client.Dispense("tasklauncher") 83 | require.NoError(err) 84 | fmt.Printf("=> %T\n", raw) 85 | bt := raw.(component.TaskLauncher) 86 | f := bt.WatchTaskFunc().(*argmapper.Func) 87 | require.NotNil(f) 88 | 89 | result := f.Call( 90 | argmapper.Typed(context.Background()), 91 | argmapper.Typed(&pb.Args_Source{App: "foo"}), 92 | ) 93 | require.NoError(result.Err()) 94 | 95 | raw = result.Out(0) 96 | require.NotNil(raw) 97 | 98 | taskResult, ok := raw.(*component.TaskResult) 99 | require.True(ok) 100 | require.True(called) 101 | require.Equal(int(42), taskResult.ExitCode) 102 | } 103 | 104 | func TestTaskLauncherConfig(t *testing.T) { 105 | mockV := &mockTaskLauncherConfigurable{} 106 | testConfigurable(t, "tasklauncher", mockV, &mockV.Configurable) 107 | } 108 | 109 | type mockTaskLauncherConfigurable struct { 110 | mocks.TaskLauncher 111 | mocks.Configurable 112 | } 113 | -------------------------------------------------------------------------------- /internal/plugin/template.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/iancoleman/strcase" 9 | "github.com/mitchellh/mapstructure" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | 13 | "github.com/hashicorp/waypoint-plugin-sdk/component" 14 | ) 15 | 16 | // templateData returns the template data for a result object. If v 17 | // implements component.Template that value is used. Otherwise, we automatically 18 | // infer the fields based on the exported fields of the struct. 19 | func templateData(v interface{}) ([]byte, error) { 20 | // Determine our data 21 | var data map[string]interface{} 22 | if tpl, ok := v.(component.Template); ok { 23 | data = tpl.TemplateData() 24 | } else { 25 | data = templateDataFromConfig(v) 26 | } 27 | 28 | // If empty we don't do anything 29 | if len(data) == 0 { 30 | return nil, nil 31 | } 32 | 33 | // Encode as JSON 34 | encoded, err := json.Marshal(data) 35 | if err != nil { 36 | return nil, status.Errorf(codes.Aborted, 37 | "failed to JSON encode result template data: %s", err) 38 | } 39 | 40 | return encoded, nil 41 | } 42 | 43 | func templateDataFromConfig(v interface{}) map[string]interface{} { 44 | var result map[string]interface{} 45 | dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 46 | Result: &result, 47 | DecodeHook: func(srcT, dstT reflect.Type, raw interface{}) (interface{}, error) { 48 | for srcT.Kind() == reflect.Ptr { 49 | srcT = srcT.Elem() 50 | } 51 | if srcT.Kind() != reflect.Struct { 52 | return raw, nil 53 | } 54 | 55 | val := reflect.ValueOf(raw) 56 | for val.Kind() == reflect.Ptr { 57 | val = val.Elem() 58 | } 59 | 60 | m := map[string]interface{}{} 61 | for i := 0; i < srcT.NumField(); i++ { 62 | sf := srcT.Field(i) 63 | if sf.PkgPath != "" { 64 | // ignore unexported fields 65 | continue 66 | } 67 | 68 | if strings.HasPrefix(sf.Name, "XXX_") { 69 | // ignore proto internals 70 | continue 71 | } 72 | 73 | name := strcase.ToSnake(sf.Name) 74 | m[name] = val.Field(i).Interface() 75 | } 76 | 77 | return m, nil 78 | }, 79 | }) 80 | if err != nil { 81 | panic(err) 82 | } 83 | if err := dec.Decode(v); err != nil { 84 | panic(err) 85 | } 86 | 87 | return result 88 | } 89 | -------------------------------------------------------------------------------- /internal/plugin/template_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestTemplateDataFromConfig(t *testing.T) { 10 | cases := []struct { 11 | Name string 12 | Input interface{} 13 | Output map[string]interface{} 14 | }{ 15 | { 16 | "all-in-one", 17 | struct { 18 | Name string 19 | Port int 20 | ImageName string 21 | }{ 22 | Name: "Hello", 23 | Port: 80, 24 | ImageName: "foo", 25 | }, 26 | map[string]interface{}{ 27 | "name": "Hello", 28 | "port": 80, 29 | "image_name": "foo", 30 | }, 31 | }, 32 | 33 | { 34 | "pointer", 35 | &struct { 36 | Name string 37 | Port int 38 | }{ 39 | Name: "Hello", 40 | Port: 80, 41 | }, 42 | map[string]interface{}{ 43 | "name": "Hello", 44 | "port": 80, 45 | }, 46 | }, 47 | 48 | { 49 | "protobuf fields", 50 | &struct { 51 | Name string 52 | XXX_Hello string 53 | }{ 54 | Name: "Hello", 55 | XXX_Hello: "hi", 56 | }, 57 | map[string]interface{}{ 58 | "name": "Hello", 59 | }, 60 | }, 61 | } 62 | 63 | for _, tt := range cases { 64 | t.Run(tt.Name, func(t *testing.T) { 65 | require := require.New(t) 66 | 67 | v := templateDataFromConfig(tt.Input) 68 | require.Equal(tt.Output, v) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/pluginargs/pluginargs.go: -------------------------------------------------------------------------------- 1 | // Package pluginargs 2 | package pluginargs 3 | 4 | import ( 5 | "github.com/hashicorp/go-argmapper" 6 | "github.com/hashicorp/go-plugin" 7 | ) 8 | 9 | // Internal is a struct that is available to mappers. This is an internal-only 10 | // type that is not possible for plugins to register for since it is only 11 | // exported in an internal package. 12 | type Internal struct { 13 | Broker *plugin.GRPCBroker 14 | Mappers []*argmapper.Func 15 | Cleanup *Cleanup 16 | } 17 | 18 | // Cleanup can be used to register cleanup functions. 19 | type Cleanup struct { 20 | f func() 21 | } 22 | 23 | // Do registers a cleanup function that will be called when the plugin RPC 24 | // call is complete. 25 | func (c *Cleanup) Do(f func()) { 26 | oldF := c.f 27 | c.f = func() { 28 | if oldF != nil { 29 | defer oldF() 30 | } 31 | f() 32 | } 33 | } 34 | 35 | func (c *Cleanup) Close() error { 36 | if c.f != nil { 37 | c.f() 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/plugincomponent/access.go: -------------------------------------------------------------------------------- 1 | package plugincomponent 2 | 3 | import ( 4 | "github.com/hashicorp/opaqueany" 5 | "google.golang.org/protobuf/proto" 6 | ) 7 | 8 | // AccessInfo provides raw access the value returned by AccessInfoFunc 9 | // as an any, to allow it to be decoded by a target plugin that needs it. 10 | type AccessInfo struct { 11 | Any *opaqueany.Any 12 | } 13 | 14 | func (c *AccessInfo) Proto() proto.Message { return c.Any } 15 | func (c *AccessInfo) TypedAny() *opaqueany.Any { return c.Any } 16 | -------------------------------------------------------------------------------- /internal/plugincomponent/artifact.go: -------------------------------------------------------------------------------- 1 | package plugincomponent 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/hashicorp/opaqueany" 7 | "google.golang.org/protobuf/proto" 8 | 9 | "github.com/hashicorp/waypoint-plugin-sdk/component" 10 | ) 11 | 12 | // Artifact implements component.Artifact. 13 | type Artifact struct { 14 | Any *opaqueany.Any 15 | AnyJson string 16 | LabelsVal map[string]string 17 | TemplateVal map[string]interface{} 18 | } 19 | 20 | func (c *Artifact) Proto() proto.Message { return c.Any } 21 | 22 | func (c *Artifact) Labels() map[string]string { return c.LabelsVal } 23 | 24 | func (c *Artifact) TemplateData() map[string]interface{} { return c.TemplateVal } 25 | 26 | func (c *Artifact) MarshalJSON() ([]byte, error) { return []byte(c.AnyJson), nil } 27 | 28 | var ( 29 | _ component.Artifact = (*Artifact)(nil) 30 | _ component.Template = (*Artifact)(nil) 31 | _ json.Marshaler = (*Artifact)(nil) 32 | ) 33 | -------------------------------------------------------------------------------- /internal/plugincomponent/deployment.go: -------------------------------------------------------------------------------- 1 | package plugincomponent 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/hashicorp/opaqueany" 7 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 8 | 9 | "github.com/hashicorp/waypoint-plugin-sdk/component" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | // Deployment implements component.Deployment. 14 | type Deployment struct { 15 | Any *opaqueany.Any 16 | AnyJson string 17 | Deployment *pb.Deploy 18 | TemplateVal map[string]interface{} 19 | } 20 | 21 | func (c *Deployment) Proto() proto.Message { return c.Any } 22 | func (c *Deployment) URL() string { 23 | if c.Deployment == nil { 24 | return "" 25 | } 26 | return c.Deployment.Url 27 | } 28 | func (c *Deployment) String() string { return "" } 29 | func (c *Deployment) TemplateData() map[string]interface{} { return c.TemplateVal } 30 | 31 | func (c *Deployment) MarshalJSON() ([]byte, error) { return []byte(c.AnyJson), nil } 32 | 33 | var ( 34 | _ component.Deployment = (*Deployment)(nil) 35 | _ component.Template = (*Deployment)(nil) 36 | _ json.Marshaler = (*Deployment)(nil) 37 | ) 38 | -------------------------------------------------------------------------------- /internal/plugincomponent/doc.go: -------------------------------------------------------------------------------- 1 | // Package plugincomponent has helpers to convert or expose component 2 | // implementations for plugin proto structures. 3 | package plugincomponent 4 | -------------------------------------------------------------------------------- /internal/plugincomponent/release.go: -------------------------------------------------------------------------------- 1 | package plugincomponent 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/hashicorp/opaqueany" 7 | "google.golang.org/protobuf/proto" 8 | 9 | "github.com/hashicorp/waypoint-plugin-sdk/component" 10 | pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" 11 | ) 12 | 13 | // Release implements component.Release. 14 | type Release struct { 15 | Any *opaqueany.Any 16 | AnyJson string 17 | Release *pb.Release 18 | TemplateVal map[string]interface{} 19 | } 20 | 21 | func (c *Release) Proto() proto.Message { return c.Any } 22 | func (c *Release) URL() string { return c.Release.Url } 23 | func (c *Release) TemplateData() map[string]interface{} { return c.TemplateVal } 24 | func (c *Release) MarshalJSON() ([]byte, error) { return []byte(c.AnyJson), nil } 25 | 26 | var ( 27 | _ component.Release = (*Release)(nil) 28 | _ component.Template = (*Release)(nil) 29 | _ json.Marshaler = (*Release)(nil) 30 | ) 31 | -------------------------------------------------------------------------------- /internal/plugincomponent/task_info.go: -------------------------------------------------------------------------------- 1 | package plugincomponent 2 | 3 | import ( 4 | "github.com/hashicorp/opaqueany" 5 | "google.golang.org/protobuf/proto" 6 | ) 7 | 8 | type RunningTask struct { 9 | Any *opaqueany.Any 10 | } 11 | 12 | func (c *RunningTask) Proto() proto.Message { return c.Any } 13 | -------------------------------------------------------------------------------- /internal/stdio/stdio.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package stdio 5 | 6 | import ( 7 | "os" 8 | "sync" 9 | ) 10 | 11 | // NOTE(mitchellh): windows is in the _windows.go file suffix 12 | 13 | // We cache the stdout/stderr files because we need to use the same *os.File 14 | // or we'll get a hang. 15 | var ( 16 | once sync.Once 17 | stdout, stderr *os.File 18 | ) 19 | 20 | // Stdout returns the stdout file that was passed as an extra file descriptor 21 | // to the plugin. We do this so that we can get access to a real TTY if 22 | // possible for subprocess output. 23 | func Stdout() *os.File { 24 | once.Do(initFds) 25 | return stdout 26 | } 27 | 28 | // Stderr. See stdout for details. 29 | func Stderr() *os.File { 30 | once.Do(initFds) 31 | return stderr 32 | } 33 | 34 | func initFds() { 35 | stdout = os.NewFile(uintptr(3), "stdout") 36 | stderr = os.NewFile(uintptr(3), "stdout") 37 | } 38 | -------------------------------------------------------------------------------- /internal/stdio/stdio_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package stdio 5 | 6 | import ( 7 | "os" 8 | "sync" 9 | "syscall" 10 | ) 11 | 12 | // We cache the stdout/stderr files because we need to use the same *os.File 13 | // or we'll get a hang. 14 | var ( 15 | once sync.Once 16 | stdout, stderr *os.File 17 | ) 18 | 19 | // Stdout returns the stdout file that was passed as an extra file descriptor 20 | // to the plugin. We do this so that we can get access to a real TTY if 21 | // possible for subprocess output. 22 | func Stdout() *os.File { 23 | once.Do(initFds) 24 | return stdout 25 | } 26 | 27 | // Stderr. See stdout for details. 28 | func Stderr() *os.File { 29 | once.Do(initFds) 30 | return stderr 31 | } 32 | 33 | func initFds() { 34 | stdout = openConsole("CONOUT$") 35 | stderr = stdout 36 | } 37 | 38 | // This is used to get the exact console handle instead of the redirected 39 | // handles from panicwrap. 40 | func openConsole(name string) *os.File { 41 | // Convert to UTF16 42 | path, err := syscall.UTF16PtrFromString(name) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | // Determine the share mode 48 | var shareMode uint32 49 | switch name { 50 | case "CONIN$": 51 | shareMode = syscall.FILE_SHARE_READ 52 | case "CONOUT$": 53 | shareMode = syscall.FILE_SHARE_WRITE 54 | } 55 | 56 | // Get the file 57 | h, err := syscall.CreateFile( 58 | path, 59 | syscall.GENERIC_READ|syscall.GENERIC_WRITE, 60 | shareMode, 61 | nil, 62 | syscall.OPEN_EXISTING, 63 | 0, 0) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | // Create the Go file 69 | return os.NewFile(uintptr(h), name) 70 | } 71 | -------------------------------------------------------------------------------- /internal/testproto/testproto.go: -------------------------------------------------------------------------------- 1 | // Package testproto contains some protobuf defintions that are used 2 | // in internal tests. 3 | package testproto 4 | 5 | //go:generate sh -c "protoc -I ./ ./*.proto --go_out=plugins=grpc:./" 6 | -------------------------------------------------------------------------------- /internal/testproto/testproto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package testproto; 4 | option go_package = '.;testproto'; 5 | 6 | // Data is just some data, used for tests so meant to be meaningless. 7 | message Data { 8 | string value = 1; 9 | int32 number = 2; 10 | } 11 | 12 | // Other types that we can use. There isn't any purpose for this other 13 | // than to provide message types that can be used for tests. 14 | message A { int32 value = 1; } 15 | message B { int32 value = 2; } 16 | -------------------------------------------------------------------------------- /proto/gen/unused.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.17.3 5 | // source: unused.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | ) 14 | 15 | const ( 16 | // Verify that this generated code is sufficiently up-to-date. 17 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 18 | // Verify that runtime/protoimpl is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 20 | ) 21 | 22 | var File_unused_proto protoreflect.FileDescriptor 23 | 24 | var file_unused_proto_rawDesc = []byte{ 25 | 0x0a, 0x0c, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 26 | 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x77, 0x61, 0x79, 0x70, 0x6f, 0x69, 27 | 0x6e, 0x74, 0x2e, 0x73, 0x64, 0x6b, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 28 | 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 29 | } 30 | 31 | var file_unused_proto_goTypes = []interface{}{} 32 | var file_unused_proto_depIdxs = []int32{ 33 | 0, // [0:0] is the sub-list for method output_type 34 | 0, // [0:0] is the sub-list for method input_type 35 | 0, // [0:0] is the sub-list for extension type_name 36 | 0, // [0:0] is the sub-list for extension extendee 37 | 0, // [0:0] is the sub-list for field type_name 38 | } 39 | 40 | func init() { file_unused_proto_init() } 41 | func file_unused_proto_init() { 42 | if File_unused_proto != nil { 43 | return 44 | } 45 | type x struct{} 46 | out := protoimpl.TypeBuilder{ 47 | File: protoimpl.DescBuilder{ 48 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 49 | RawDescriptor: file_unused_proto_rawDesc, 50 | NumEnums: 0, 51 | NumMessages: 0, 52 | NumExtensions: 0, 53 | NumServices: 0, 54 | }, 55 | GoTypes: file_unused_proto_goTypes, 56 | DependencyIndexes: file_unused_proto_depIdxs, 57 | }.Build() 58 | File_unused_proto = out.File 59 | file_unused_proto_rawDesc = nil 60 | file_unused_proto_goTypes = nil 61 | file_unused_proto_depIdxs = nil 62 | } 63 | -------------------------------------------------------------------------------- /proto/unused.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package hashicorp.waypoint.sdk; 4 | 5 | option go_package = "./;proto"; 6 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | ( 2 | import (fetchTarball https://github.com/edolstra/flake-compat/archive/master.tar.gz) { 3 | src = builtins.fetchGit ./.; 4 | } 5 | ).shellNix 6 | -------------------------------------------------------------------------------- /terminal/doc.go: -------------------------------------------------------------------------------- 1 | // Package terminal is used by plugins to read and write to a terminal 2 | // UI. The abstractions presented in this package are meant to be portable 3 | // across a variety of styles that may be presented to the user and enables 4 | // plugins to focus on their core logic. 5 | // 6 | // The primary interface to look at is UI. 7 | package terminal 8 | -------------------------------------------------------------------------------- /terminal/glint_status.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/mitchellh/go-glint" 8 | gc "github.com/mitchellh/go-glint/components" 9 | ) 10 | 11 | // glintStatus implements Status and uses a spinner to show updates. 12 | type glintStatus struct { 13 | mu sync.Mutex 14 | closed bool 15 | msg string 16 | spinner glint.Component 17 | text []glint.Component 18 | } 19 | 20 | func newGlintStatus() *glintStatus { 21 | return &glintStatus{ 22 | spinner: gc.Spinner(), 23 | } 24 | } 25 | 26 | func (s *glintStatus) Update(msg string) { 27 | s.mu.Lock() 28 | defer s.mu.Unlock() 29 | s.msg = msg 30 | } 31 | 32 | func (s *glintStatus) Step(status, msg string) { 33 | s.mu.Lock() 34 | defer s.mu.Unlock() 35 | 36 | // Determine our color 37 | var style []glint.StyleOption 38 | switch status { 39 | case StatusOK: 40 | style = append(style, glint.Color("lightGreen")) 41 | 42 | case StatusError: 43 | style = append(style, glint.Color("lightRed")) 44 | 45 | case StatusWarn: 46 | style = append(style, glint.Color("lightYellow")) 47 | } 48 | 49 | // If we have a prefix, set that 50 | if icon, ok := statusIcons[status]; ok { 51 | msg = icon + " " + msg 52 | } 53 | 54 | // Clear the message so we don't draw a spinner 55 | s.msg = "" 56 | 57 | // Add our final message 58 | s.text = append(s.text, glint.Finalize(glint.Style( 59 | glint.Text(msg), 60 | style..., 61 | ))) 62 | } 63 | 64 | func (s *glintStatus) Close() error { 65 | s.mu.Lock() 66 | defer s.mu.Unlock() 67 | s.closed = true 68 | return nil 69 | } 70 | 71 | func (s *glintStatus) reset() { 72 | s.mu.Lock() 73 | defer s.mu.Unlock() 74 | s.text = nil 75 | s.msg = "" 76 | } 77 | 78 | func (s *glintStatus) Body(context.Context) glint.Component { 79 | s.mu.Lock() 80 | defer s.mu.Unlock() 81 | 82 | var cs []glint.Component 83 | 84 | // If we have text we draw that first 85 | if len(s.text) > 0 { 86 | cs = append(cs, glint.Finalize(glint.Fragment(s.text...))) 87 | } 88 | 89 | // If we have a message the spinner is active and we draw that 90 | if !s.closed && len(s.msg) > 0 { 91 | cs = append(cs, glint.Layout( 92 | s.spinner, 93 | glint.Text(" "), 94 | glint.Text(s.msg), 95 | ).Row()) 96 | } 97 | 98 | c := glint.Fragment(cs...) 99 | if s.closed { 100 | c = glint.Finalize(c) 101 | } 102 | 103 | return c 104 | } 105 | -------------------------------------------------------------------------------- /terminal/glint_step_group.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/mitchellh/go-glint" 10 | ) 11 | 12 | // glintStepGroup implements StepGroup with live updating and a display 13 | // "window" for live terminal output (when using TermOutput). 14 | type glintStepGroup struct { 15 | mu sync.Mutex 16 | ctx context.Context 17 | cancel context.CancelFunc 18 | wg sync.WaitGroup 19 | steps []*glintStep 20 | closed bool 21 | } 22 | 23 | // Start a step in the output 24 | func (f *glintStepGroup) Add(str string, args ...interface{}) Step { 25 | f.mu.Lock() 26 | defer f.mu.Unlock() 27 | 28 | // Build our step 29 | step := &glintStep{ctx: f.ctx, status: newGlintStatus()} 30 | 31 | // Setup initial status 32 | step.Update(str, args...) 33 | 34 | // If we're closed we don't add this step to our waitgroup or document. 35 | // We still create a step and return a non-nil step so downstreams don't 36 | // crash. 37 | if !f.closed { 38 | // Add since we have a step 39 | step.wg = &f.wg 40 | f.wg.Add(1) 41 | 42 | // Add it to our list 43 | f.steps = append(f.steps, step) 44 | } 45 | 46 | return step 47 | } 48 | 49 | func (f *glintStepGroup) Wait() { 50 | f.mu.Lock() 51 | f.closed = true 52 | f.cancel() 53 | wg := &f.wg 54 | f.mu.Unlock() 55 | 56 | wg.Wait() 57 | } 58 | 59 | func (f *glintStepGroup) Body(context.Context) glint.Component { 60 | f.mu.Lock() 61 | defer f.mu.Unlock() 62 | 63 | var cs []glint.Component 64 | for _, s := range f.steps { 65 | cs = append(cs, s) 66 | } 67 | 68 | return glint.Fragment(cs...) 69 | } 70 | 71 | type glintStep struct { 72 | mu sync.Mutex 73 | ctx context.Context 74 | wg *sync.WaitGroup 75 | done bool 76 | msg string 77 | statusVal string 78 | status *glintStatus 79 | term *glintTerm 80 | } 81 | 82 | func (f *glintStep) TermOutput() io.Writer { 83 | f.mu.Lock() 84 | defer f.mu.Unlock() 85 | 86 | if f.term == nil { 87 | t, err := newGlintTerm(f.ctx, 10, 80) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | f.term = t 93 | } 94 | 95 | return f.term 96 | } 97 | 98 | func (f *glintStep) Update(str string, args ...interface{}) { 99 | f.mu.Lock() 100 | defer f.mu.Unlock() 101 | f.msg = fmt.Sprintf(str, args...) 102 | f.status.reset() 103 | 104 | if f.statusVal != "" { 105 | f.status.Step(f.statusVal, f.msg) 106 | } else { 107 | f.status.Update(f.msg) 108 | } 109 | } 110 | 111 | func (f *glintStep) Status(status string) { 112 | f.mu.Lock() 113 | defer f.mu.Unlock() 114 | f.statusVal = status 115 | f.status.reset() 116 | f.status.Step(status, f.msg) 117 | } 118 | 119 | func (f *glintStep) Done() { 120 | f.mu.Lock() 121 | defer f.mu.Unlock() 122 | 123 | if f.done { 124 | return 125 | } 126 | 127 | // Set done 128 | f.done = true 129 | 130 | // Set status 131 | if f.statusVal == "" { 132 | f.status.reset() 133 | f.status.Step(StatusOK, f.msg) 134 | } 135 | 136 | // Unset the waitgroup 137 | f.wg.Done() 138 | } 139 | 140 | func (f *glintStep) Abort() { 141 | f.mu.Lock() 142 | defer f.mu.Unlock() 143 | 144 | if f.done { 145 | return 146 | } 147 | 148 | f.done = true 149 | 150 | // This will cause the term to render the full scrollback from now on 151 | if f.term != nil { 152 | f.term.showFull() 153 | } 154 | 155 | f.status.Step(StatusError, f.msg) 156 | f.wg.Done() 157 | } 158 | 159 | func (f *glintStep) Body(context.Context) glint.Component { 160 | f.mu.Lock() 161 | defer f.mu.Unlock() 162 | 163 | var cs []glint.Component 164 | cs = append(cs, f.status) 165 | 166 | // If we have a terminal, output that too. 167 | if f.term != nil { 168 | cs = append(cs, f.term) 169 | } 170 | 171 | return glint.Fragment(cs...) 172 | } 173 | -------------------------------------------------------------------------------- /terminal/glint_term.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "strings" 8 | "sync" 9 | "unicode" 10 | 11 | "github.com/lab47/vterm/parser" 12 | "github.com/lab47/vterm/screen" 13 | "github.com/lab47/vterm/state" 14 | "github.com/mitchellh/go-glint" 15 | ) 16 | 17 | type glintTerm struct { 18 | mu sync.Mutex 19 | 20 | w io.Writer 21 | scr *screen.Screen 22 | ctx context.Context 23 | cancel func() 24 | 25 | output [][]rune 26 | height, width int 27 | 28 | wg sync.WaitGroup 29 | parseErr error 30 | 31 | scrollback [][]rune 32 | 33 | full bool 34 | } 35 | 36 | func (t *glintTerm) Body(ctx context.Context) glint.Component { 37 | t.mu.Lock() 38 | defer t.mu.Unlock() 39 | 40 | var cs []glint.Component 41 | 42 | if t.full { 43 | for _, row := range t.scrollback { 44 | s := strings.TrimRightFunc(string(row), unicode.IsSpace) 45 | cs = append(cs, glint.Layout(glint.Text(" │ "), glint.Text(s)).Row()) 46 | } 47 | } 48 | 49 | for _, row := range t.output { 50 | cs = append(cs, glint.Layout( 51 | glint.Text(" │ "), 52 | glint.Style( 53 | glint.Text(strings.TrimRightFunc(string(row), unicode.IsSpace)), 54 | glint.Color("lightBlue"), 55 | ), 56 | ).Row()) 57 | } 58 | 59 | return glint.Fragment(cs...) 60 | } 61 | 62 | func (t *glintTerm) DamageDone(r state.Rect, cr screen.CellReader) error { 63 | t.mu.Lock() 64 | defer t.mu.Unlock() 65 | 66 | for row := r.Start.Row; row <= r.End.Row; row++ { 67 | for col := r.Start.Col; col <= r.End.Col; col++ { 68 | cell := cr.GetCell(row, col) 69 | 70 | for len(t.output) <= row { 71 | t.output = append(t.output, make([]rune, t.width)) 72 | } 73 | 74 | if cell == nil { 75 | t.output[row][col] = ' ' 76 | } else { 77 | val, _ := cell.Value() 78 | 79 | if val == 0 { 80 | t.output[row][col] = ' ' 81 | } else { 82 | t.output[row][col] = val 83 | } 84 | } 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (t *glintTerm) MoveCursor(p state.Pos) error { 92 | // Ignore it. 93 | return nil 94 | } 95 | 96 | func (t *glintTerm) SetTermProp(attr state.TermAttr, val interface{}) error { 97 | // Ignore it. 98 | return nil 99 | } 100 | 101 | func (t *glintTerm) Output(data []byte) error { 102 | // Ignore it. 103 | return nil 104 | } 105 | 106 | func (t *glintTerm) StringEvent(kind string, data []byte) error { 107 | // Ignore them. 108 | return nil 109 | } 110 | 111 | func newGlintTerm(ctx context.Context, height, width int) (*glintTerm, error) { 112 | term := &glintTerm{ 113 | height: height, 114 | width: width, 115 | } 116 | 117 | scr, err := screen.NewScreen(height, width, term) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | term.scr = scr 123 | 124 | st, err := state.NewState(height, width, scr) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | r, w, err := os.Pipe() 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | term.w = w 135 | 136 | prs, err := parser.NewParser(r, st) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | term.ctx, term.cancel = context.WithCancel(ctx) 142 | 143 | term.wg.Add(1) 144 | go func() { 145 | defer term.wg.Done() 146 | 147 | err := prs.Drive(term.ctx) 148 | if err != nil && err != context.Canceled { 149 | term.parseErr = err 150 | } 151 | }() 152 | 153 | return term, nil 154 | } 155 | 156 | func (t *glintTerm) Write(b []byte) (int, error) { 157 | return t.w.Write(b) 158 | } 159 | 160 | func (t *glintTerm) AddScrollBack(line []rune) error { 161 | t.mu.Lock() 162 | defer t.mu.Unlock() 163 | 164 | t.scrollback = append(t.scrollback, line) 165 | return nil 166 | } 167 | 168 | func (t *glintTerm) showFull() { 169 | t.mu.Lock() 170 | defer t.mu.Unlock() 171 | 172 | t.full = true 173 | } 174 | 175 | var _ screen.ScrollBack = (*glintTerm)(nil) 176 | 177 | func (t *glintTerm) Close() error { 178 | t.cancel() 179 | t.wg.Wait() 180 | return t.parseErr 181 | } 182 | -------------------------------------------------------------------------------- /terminal/input.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // Input is the configuration for an input. 4 | type Input struct { 5 | // Prompt is a single-line prompt to give the user such as "Continue?" 6 | // The user will input their answer after this prompt. 7 | Prompt string 8 | 9 | // Style is the style to apply to the input. If this is blank, 10 | // the output won't be colorized in any way. 11 | Style string 12 | 13 | // True if this input is a secret. The input will be masked. 14 | Secret bool 15 | } 16 | -------------------------------------------------------------------------------- /terminal/status.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/fatih/color" 12 | "github.com/hashicorp/waypoint-plugin-sdk/internal/pkg/spinner" 13 | "github.com/morikuni/aec" 14 | ) 15 | 16 | const ( 17 | StatusOK = "ok" 18 | StatusError = "error" 19 | StatusWarn = "warn" 20 | StatusTimeout = "timeout" 21 | StatusAbort = "abort" 22 | ) 23 | 24 | var emojiStatus = map[string]string{ 25 | StatusOK: "\u2713", 26 | StatusError: "❌", 27 | StatusWarn: "⚠️", 28 | StatusTimeout: "⌛", 29 | } 30 | 31 | var textStatus = map[string]string{ 32 | StatusOK: " +", 33 | StatusError: " !", 34 | StatusWarn: " *", 35 | StatusTimeout: "<>", 36 | } 37 | 38 | var colorStatus = map[string][]aec.ANSI{ 39 | StatusOK: {aec.GreenF}, 40 | StatusError: {aec.RedF}, 41 | StatusWarn: {aec.YellowF}, 42 | } 43 | 44 | // Status is used to provide an updating status to the user. The status 45 | // usually has some animated element along with it such as a spinner. 46 | type Status interface { 47 | // Update writes a new status. This should be a single line. 48 | Update(msg string) 49 | 50 | // Indicate that a step has finished, confering an ok, error, or warn upon 51 | // it's finishing state. If the status is not StatusOK, StatusError, or StatusWarn 52 | // then the status text is written directly to the output, allowing for custom 53 | // statuses. 54 | Step(status, msg string) 55 | 56 | // Close should be called when the live updating is complete. The 57 | // status will be cleared from the line. 58 | Close() error 59 | } 60 | 61 | // spinnerStatus implements Status and uses a spinner to show updates. 62 | type spinnerStatus struct { 63 | mu sync.Mutex 64 | spinner *spinner.Spinner 65 | running bool 66 | } 67 | 68 | var statusIcons map[string]string 69 | 70 | const envForceEmoji = "WAYPOINT_FORCE_EMOJI" 71 | 72 | func init() { 73 | if os.Getenv(envForceEmoji) != "" || strings.Contains(os.Getenv("LANG"), "UTF-8") { 74 | statusIcons = emojiStatus 75 | } else { 76 | statusIcons = textStatus 77 | } 78 | } 79 | 80 | func newSpinnerStatus(ctx context.Context) *spinnerStatus { 81 | return &spinnerStatus{ 82 | spinner: spinner.New( 83 | ctx, 84 | spinner.CharSets[11], 85 | time.Second/6, 86 | spinner.WithColor("bold"), 87 | ), 88 | } 89 | } 90 | 91 | func (s *spinnerStatus) Update(msg string) { 92 | s.mu.Lock() 93 | defer s.mu.Unlock() 94 | 95 | s.spinner.Suffix = " " + msg 96 | 97 | if !s.running { 98 | s.spinner.Start() 99 | s.running = true 100 | } 101 | } 102 | 103 | func (s *spinnerStatus) Step(status, msg string) { 104 | s.mu.Lock() 105 | defer s.mu.Unlock() 106 | 107 | s.spinner.Stop() 108 | s.running = false 109 | 110 | pad := "" 111 | 112 | statusIcon := emojiStatus[status] 113 | if statusIcon == "" { 114 | statusIcon = status 115 | } else if status == StatusWarn { 116 | pad = " " 117 | } 118 | 119 | fmt.Fprintf(color.Output, "%s%s %s\n", statusIcon, pad, msg) 120 | } 121 | 122 | func (s *spinnerStatus) Close() error { 123 | s.mu.Lock() 124 | defer s.mu.Unlock() 125 | 126 | if s.running { 127 | s.running = false 128 | s.spinner.Suffix = "" 129 | } 130 | 131 | s.spinner.Stop() 132 | 133 | return nil 134 | } 135 | 136 | func (s *spinnerStatus) Pause() bool { 137 | s.mu.Lock() 138 | defer s.mu.Unlock() 139 | 140 | wasRunning := s.running 141 | 142 | if s.running { 143 | s.running = false 144 | s.spinner.Stop() 145 | } 146 | 147 | return wasRunning 148 | } 149 | 150 | func (s *spinnerStatus) Start() { 151 | s.mu.Lock() 152 | defer s.mu.Unlock() 153 | 154 | if !s.running { 155 | s.running = true 156 | s.spinner.Start() 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /terminal/step.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | const ( 9 | TermRows = 10 10 | TermColumns = 100 11 | ) 12 | 13 | // fancyStepGroup implements StepGroup with live updating and a display 14 | // "window" for live terminal output (when using TermOutput). 15 | type fancyStepGroup struct { 16 | ctx context.Context 17 | cancel func() 18 | 19 | display *Display 20 | 21 | steps int 22 | done chan struct{} 23 | } 24 | 25 | // Start a step in the output 26 | func (f *fancyStepGroup) Add(str string, args ...interface{}) Step { 27 | f.steps++ 28 | 29 | ent := f.display.NewStatus(0) 30 | 31 | ent.StartSpinner() 32 | ent.Update(str, args...) 33 | 34 | return &fancyStep{ 35 | sg: f, 36 | ent: ent, 37 | } 38 | } 39 | 40 | func (f *fancyStepGroup) Wait() { 41 | if f.steps > 0 { 42 | loop: 43 | for { 44 | select { 45 | case <-f.done: 46 | f.steps-- 47 | 48 | if f.steps <= 0 { 49 | break loop 50 | } 51 | case <-f.ctx.Done(): 52 | break loop 53 | } 54 | } 55 | } 56 | 57 | f.cancel() 58 | 59 | f.display.Close() 60 | } 61 | 62 | type fancyStep struct { 63 | sg *fancyStepGroup 64 | ent *DisplayEntry 65 | 66 | done bool 67 | status string 68 | 69 | term *Term 70 | } 71 | 72 | func (f *fancyStep) TermOutput() io.Writer { 73 | if f.term == nil { 74 | t, err := NewTerm(f.sg.ctx, f.ent, TermRows, TermColumns) 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | f.term = t 80 | } 81 | 82 | return f.term 83 | } 84 | 85 | func (f *fancyStep) Update(str string, args ...interface{}) { 86 | f.ent.Update(str, args...) 87 | } 88 | 89 | func (f *fancyStep) Status(status string) { 90 | f.status = status 91 | f.ent.SetStatus(status) 92 | } 93 | 94 | func (f *fancyStep) Done() { 95 | if f.done { 96 | return 97 | } 98 | 99 | if f.status == "" { 100 | f.Status(StatusOK) 101 | } 102 | 103 | f.signalDone() 104 | } 105 | 106 | func (f *fancyStep) Abort() { 107 | if f.done { 108 | return 109 | } 110 | 111 | f.Status(StatusError) 112 | f.signalDone() 113 | } 114 | 115 | func (f *fancyStep) signalDone() { 116 | f.done = true 117 | f.ent.StopSpinner() 118 | 119 | // We don't want to block here because Wait might not yet have been 120 | // called. So instead we just spawn the wait update in a goroutine 121 | // that can also be cancaled by the context. 122 | go func() { 123 | select { 124 | case f.sg.done <- struct{}{}: 125 | case <-f.sg.ctx.Done(): 126 | return 127 | } 128 | }() 129 | } 130 | -------------------------------------------------------------------------------- /terminal/table.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | "github.com/olekukonko/tablewriter" 6 | ) 7 | 8 | // Passed to UI.Table to provide a nicely formatted table. 9 | type Table struct { 10 | Headers []string 11 | Rows [][]TableEntry 12 | } 13 | 14 | // Table creates a new Table structure that can be used with UI.Table. 15 | func NewTable(headers ...string) *Table { 16 | return &Table{ 17 | Headers: headers, 18 | } 19 | } 20 | 21 | // TableEntry is a single entry for a table. 22 | type TableEntry struct { 23 | Value string 24 | Color string 25 | } 26 | 27 | // Rich adds a row to the table. 28 | func (t *Table) Rich(cols []string, colors []string) { 29 | var row []TableEntry 30 | 31 | for i, col := range cols { 32 | if i < len(colors) { 33 | row = append(row, TableEntry{Value: col, Color: colors[i]}) 34 | } else { 35 | row = append(row, TableEntry{Value: col}) 36 | } 37 | } 38 | 39 | t.Rows = append(t.Rows, row) 40 | } 41 | 42 | // Table implements UI 43 | func (u *basicUI) Table(tbl *Table, opts ...Option) { 44 | // Build our config and set our options 45 | cfg := &config{Writer: color.Output} 46 | for _, opt := range opts { 47 | opt(cfg) 48 | } 49 | 50 | table := tablewriter.NewWriter(cfg.Writer) 51 | 52 | table.SetHeader(tbl.Headers) 53 | table.SetBorder(false) 54 | table.SetAutoWrapText(true) 55 | 56 | if cfg.Style == "Simple" { 57 | // Format the table without borders, simple output 58 | 59 | table.SetAutoFormatHeaders(true) 60 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 61 | table.SetAlignment(tablewriter.ALIGN_LEFT) 62 | table.SetCenterSeparator("") 63 | table.SetColumnSeparator("") 64 | table.SetRowSeparator("") 65 | table.SetHeaderLine(false) 66 | table.SetTablePadding("\t") // pad with tabs 67 | table.SetNoWhiteSpace(true) 68 | } 69 | 70 | for _, row := range tbl.Rows { 71 | colors := make([]tablewriter.Colors, len(row)) 72 | entries := make([]string, len(row)) 73 | 74 | for i, ent := range row { 75 | entries[i] = ent.Value 76 | 77 | color, ok := colorMapping[ent.Color] 78 | if ok { 79 | colors[i] = tablewriter.Colors{color} 80 | } 81 | } 82 | 83 | table.Rich(entries, colors) 84 | } 85 | 86 | table.Render() 87 | } 88 | 89 | const ( 90 | Yellow = "yellow" 91 | Green = "green" 92 | Red = "red" 93 | ) 94 | 95 | var colorMapping = map[string]int{ 96 | Green: tablewriter.FgGreenColor, 97 | Yellow: tablewriter.FgYellowColor, 98 | Red: tablewriter.FgRedColor, 99 | } 100 | -------------------------------------------------------------------------------- /terminal/ui_test.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNamedValues(t *testing.T) { 12 | require := require.New(t) 13 | 14 | var buf bytes.Buffer 15 | var ui basicUI 16 | ui.NamedValues([]NamedValue{ 17 | {"hello", "a"}, 18 | {"this", "is"}, 19 | {"a", "test"}, 20 | {"of", "foo"}, 21 | {"the_key_value", "style"}, 22 | }, 23 | WithWriter(&buf), 24 | ) 25 | 26 | expected := ` 27 | hello: a 28 | this: is 29 | a: test 30 | of: foo 31 | the_key_value: style 32 | 33 | ` 34 | 35 | require.Equal(strings.TrimLeft(expected, "\n"), buf.String()) 36 | } 37 | 38 | func TestNamedValues_server(t *testing.T) { 39 | require := require.New(t) 40 | 41 | var buf bytes.Buffer 42 | var ui basicUI 43 | ui.Output("Server configuration:", WithHeaderStyle(), WithWriter(&buf)) 44 | ui.NamedValues([]NamedValue{ 45 | {"DB Path", "data.db"}, 46 | {"gRPC Address", "127.0.0.1:1234"}, 47 | {"HTTP Address", "127.0.0.1:1235"}, 48 | {"URL Service", "api.alpha.waypoint.run:443 (account: token)"}, 49 | }, 50 | WithWriter(&buf), 51 | ) 52 | 53 | expected := ` 54 | ==> Server configuration: 55 | DB Path: data.db 56 | gRPC Address: 127.0.0.1:1234 57 | HTTP Address: 127.0.0.1:1235 58 | URL Service: api.alpha.waypoint.run:443 (account: token) 59 | 60 | ` 61 | 62 | require.Equal(expected, buf.String()) 63 | } 64 | 65 | func TestStatusStyle(t *testing.T) { 66 | require := require.New(t) 67 | 68 | var buf bytes.Buffer 69 | var ui basicUI 70 | ui.Output(strings.TrimSpace(` 71 | one 72 | two 73 | three`), 74 | WithWriter(&buf), 75 | WithInfoStyle(), 76 | ) 77 | 78 | expected := ` one 79 | two 80 | three 81 | ` 82 | 83 | require.Equal(expected, buf.String()) 84 | } 85 | -------------------------------------------------------------------------------- /tools.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17.6 2 | 3 | ARG PROTOC_VERSION="3.17.3" 4 | 5 | RUN apt-get update; apt-get install unzip 6 | 7 | # Protoc 8 | # TODO(izaak): discover the protoc version from the nix files 9 | RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-$(uname -m | sed s/aarch64/aarch_64/g).zip -O /tmp/protoc.zip && \ 10 | unzip /tmp/protoc.zip -d /tmp && \ 11 | mv /tmp/bin/protoc /usr/local/bin/ && \ 12 | chmod +x /usr/local/bin/protoc && \ 13 | mv /tmp/include/* /usr/local/include/ 14 | 15 | # Copy files required to update tooling 16 | RUN mkdir -p /tools/tools 17 | COPY ./tools/tools.go /tools/tools 18 | COPY ./Makefile /tools 19 | COPY go.mod /tools 20 | COPY go.sum /tools 21 | 22 | # Make tools 23 | RUN go generate -tags tools /tools/tools/tools.go 24 | 25 | WORKDIR /waypoint 26 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // To install the following tools at the version used by this repo run: 5 | // $ make tools 6 | // or 7 | // $ go generate -tags tools tools/tools.go 8 | 9 | package tools 10 | 11 | //go:generate go install github.com/golang/protobuf/proto 12 | //go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go 13 | //go:generate go install google.golang.org/grpc/cmd/protoc-gen-go-grpc 14 | 15 | import ( 16 | _ "github.com/golang/protobuf/proto" 17 | 18 | _ "google.golang.org/protobuf/cmd/protoc-gen-go" 19 | 20 | _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" 21 | ) 22 | --------------------------------------------------------------------------------