├── .editorconfig ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build └── package │ ├── archlinux │ └── PKGBUILD │ └── brew │ ├── relay-cli-update-brew.yaml │ └── update_formula.sh ├── cmd └── relay │ └── main.go ├── docs ├── generate.go ├── relay-logo.svg └── relay.md ├── examples └── metadata-configs │ └── simple.yaml ├── go.mod ├── go.sum ├── pkg ├── client │ ├── auth.go │ ├── client.go │ ├── request.go │ ├── revision.go │ ├── token.go │ └── workflow.go ├── cmd │ ├── auth.go │ ├── completion.go │ ├── config.go │ ├── context.go │ ├── dev.go │ ├── doc.go │ ├── main.go │ ├── main_test.go │ ├── metadata.go │ ├── notifications.go │ ├── subscriptions.go │ ├── token.go │ ├── version.go │ ├── workflow.go │ ├── workflow_save.go │ └── workflow_secret.go ├── config │ └── config.go ├── debug │ └── debug.go ├── dev │ ├── admin.go │ ├── dev.go │ ├── manifest.go │ ├── manifests │ │ ├── asset.go │ │ ├── data │ │ │ ├── helm-controller │ │ │ │ └── deploy-namespaced.yaml │ │ │ ├── knative │ │ │ │ ├── 1-serving-crds.yaml │ │ │ │ ├── 2-serving-core.yaml │ │ │ │ └── 3-patch-config.yaml │ │ │ ├── kourier │ │ │ │ └── kourier.yaml │ │ │ ├── relay │ │ │ │ ├── install.relay.sh_relaycores.yaml │ │ │ │ ├── relay.sh_runs.yaml │ │ │ │ ├── relay.sh_tenants.yaml │ │ │ │ ├── relay.sh_webhooktriggers.yaml │ │ │ │ └── relay.sh_workflows.yaml │ │ │ └── tekton │ │ │ │ └── release.yaml │ │ ├── generate.go │ │ ├── generate_assets.go │ │ └── generate_tool.go │ ├── metadata.go │ ├── namespace.go │ ├── relaycore.go │ ├── relayinstaller.go │ └── vault.go ├── dialog │ ├── dialog.go │ ├── json_table.go │ ├── progress.go │ ├── table.go │ └── text_table.go ├── errors │ ├── build.go │ ├── build_errors.go │ ├── build_tool.go │ └── errors.yaml ├── format │ ├── error.go │ └── guilink.go ├── model │ ├── revision.go │ ├── token.go │ ├── token_test.go │ └── workflow.go ├── util │ ├── confirm.go │ └── stdin.go └── version │ └── version.go ├── scripts ├── build ├── ci ├── dist ├── dist-all ├── generate ├── library.sh ├── release ├── release-all ├── run-workflow └── test └── tools.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | [*.{md,yaml,yml}] 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | generate: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/setup-go@v2 8 | with: 9 | go-version: '1.18' 10 | - uses: actions/checkout@v2 11 | - uses: actions/cache@v2 12 | with: 13 | path: | 14 | ~/go/pkg/mod 15 | ~/go/pkg/sumdb 16 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 17 | restore-keys: | 18 | ${{ runner.os }}-go- 19 | - name: Check 20 | run: | 21 | go generate ./... 22 | if [ -n "$(git status --porcelain --untracked-files=no)" ]; then 23 | git diff 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[onp] 2 | *.code-workspace 3 | /relay 4 | /.depend/ 5 | /artifacts/ 6 | /bin/ 7 | /node_modules/ 8 | 9 | *~ 10 | 11 | pkg/**/.openapi-generator 12 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/relay-community 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Guidelines and Code of Conduct 2 | 3 | We want to keep the Puppet communities awesome, and we need your help to keep it 4 | that way. While we have specific guidelines for various tools (see links below), 5 | in general, you should: 6 | 7 | * **Be nice**: Be courteous, respectful and polite to fellow community members. No 8 | offensive comments related to gender, gender identity or expression, sexual 9 | orientation, disability, physical appearance, body size, race, religion; no 10 | sexual images in public spaces, real or implied violence, intimidation, 11 | oppression, stalking, following, harassing photography or recording, sustained 12 | disruption of talks or other events, inappropriate physical contact, doxxing, or 13 | unwelcome sexual attention will be tolerated. We like nice people way better 14 | than mean ones! 15 | * **Encourage diversity and participation**: Make everyone in our community feel 16 | welcome, regardless of their background, and do everything possible to encourage 17 | participation in our community. 18 | * **Focus on constructive criticisms**: When offering suggestions, whether in online 19 | discussions or as comments on a pull request, you should always use welcoming 20 | and inclusive language. Be respectful of differing viewpoints and the fact that 21 | others may not have the same experiences you do. Offer suggestions for 22 | improvement, rather than focusing on mistakes. When others critique your work or 23 | ideas, gracefully accept the criticisms and default to assuming good intentions. 24 | * **Keep it legal**: Basically, don't get us in trouble. Share only content that you 25 | own, do not share private or sensitive information, and don't break the law. 26 | * **Stay on topic**: Keep conversation in a thread on topic, whether that's a pull 27 | request or a Slack conversation or anything else. Make sure that you are posting 28 | to the correct channel and remember that nobody likes spam. 29 | 30 | ## Guideline violations—3 strikes method 31 | 32 | The point of this section is not to find opportunities to punish people, but we 33 | do need a fair way to deal with people who do harm to our community. Extreme 34 | violations of a threatening, abusive, destructive, or illegal nature will be 35 | addressed immediately and are not subject to 3 strikes. 36 | 37 | * First occurrence: We'll give you a friendly, but public, reminder that the 38 | behavior is inappropriate according to our guidelines. 39 | * Second occurrence: We'll send you a private message with a warning that any 40 | additional violations will result in removal from the community. 41 | * Third occurrence: Depending on the violation, we might need to delete or ban 42 | your account. 43 | 44 | Notes: 45 | 46 | * Obvious spammers are banned on first occurrence. If we don’t do this, we’ll 47 | have spam all over the place. 48 | * Violations are forgiven after 6 months of good behavior, and we won’t hold a grudge. 49 | * People who are committing minor formatting / style infractions will get some 50 | education, rather than hammering them in the 3 strikes process. 51 | 52 | Contact conduct@puppet.com to report abuse or appeal violations. This email list 53 | goes to Kara Sowles (kara at puppet.com) and Katie Abbott (katie dot abbott at 54 | puppet.com). In the case of appeals, we know that mistakes happen, and we’ll 55 | work with you to come up with a fair solution if there has been a 56 | misunderstanding. 57 | 58 | ## Full text 59 | 60 | See our [full community guidelines](https://puppet.com/community/community-guidelines), 61 | covering Slack, IRC, events and other forms of community participation. 62 | 63 | ## Credits 64 | 65 | Credit to [01.org](https://01.org/community/participation-guidelines) and 66 | [meego.com](http://wiki.meego.com/Community_guidelines), since they formed the 67 | starting point for many of these guidelines. 68 | 69 | The Event Code of Conduct is based on the [example policy from the Geek Feminism wiki](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment), 70 | created by the Ada Initiative and other volunteers. The [PyCon Code of Conduct](https://github.com/python/pycon-code-of-conduct) also served as inspiration. 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Relay 2 | 3 | Relay welcomes contributions! Read on if you're interested in getting involved with the project. 4 | 5 | ## Guidelines for contributions 6 | 7 | All interactions between Puppet employees, contributors, and community members on Relay-related projects are subject to [Puppet Community Code of Conduct](https://puppet.com/community/community-guidelines/). 8 | 9 | Make sure there's not some existing code or a discussion that covers the change you want to make by searching existing Github issues. 10 | 11 | To make it easier to contribute while still staying in the good graces of our (super wonderful!) Legal department, we require a [Developer Certificate of Origin](https://developercertificate.org/) sign-off on contributions. See [this explanation](https://helm.sh/blog/helm-dco/) from the Helm project to understand the rationale behind the DCO.As a practical matter, this means adding the `-s | --signoff` flag to your commits. 12 | 13 | 14 | ## Making Changes 15 | 16 | * Clone the repository into your own namespace 17 | * Create a topic branch from where you want to base your work. 18 | * To quickly create a topic branch based on `main`, run `git checkout -b fix/my_fix origin/main`. 19 | * Make commits of logical and atomic units. 20 | * Check for unnecessary whitespace with `git diff --check` before committing. 21 | * Make sure your commit messages are in the proper format. We (try to!) follow [Tim Pope's guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for writing good commit messages: format for short lines, use the imperative mood ("Add X to Y"), describe before and after state in the commit message body. Remember to add the `-s` flag to commits to DCO-sign them! 22 | * Make sure you have added the necessary tests for your changes. 23 | * Submit a pull request per the usual github PR process. 24 | 25 | 26 | ## Additional Resources 27 | 28 | * [Puppet community guidelines](https://puppet.com/community/community-guidelines) 29 | * [Puppet community slack](https://slack.puppet.com) 30 | * [General GitHub documentation](https://help.github.com/) 31 | * [GitHub pull request documentation](https://help.github.com/articles/creating-a-pull-request/) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Relay by Puppet 3 |

4 | 5 | Relay is a service that lets you connect tools, APIs, and infrastructure to automate common tasks through simpler, smarter workflows. It links infrastructure events to workflow execution, so that for example, when a new JIRA ticket or GitHub issue comes in, your workflow can trigger deployments or send notifications. 6 | 7 | This repo contains the source for the CLI tool. 8 | 9 | ## Installation 10 | 11 | For Macs, install via homebrew: 12 | 13 | ```bash 14 | brew install puppetlabs/puppet/relay 15 | ``` 16 | 17 | For other platforms, install directly via GitHub Releases: 18 | 19 | [Get the latest version](https://github.com/puppetlabs/relay/releases) 20 | 21 | The program is just a single binary, so you can simply download the one that matches your architecture and copy it to a location in your `$PATH`. 22 | 23 | ```bash 24 | mv ./relay-v4*-linux-arm64 /usr/local/bin/relay 25 | ``` 26 | 27 | ## Getting started 28 | 29 | Once it's installed, you'll need to authenticate with the service, then you'll be able to work with the default set of workflows that are enabled on your account: 30 | 31 | ```bash 32 | relay auth login 33 | relay workflow list 34 | ``` 35 | 36 | ### Config 37 | 38 | Relay uses [viper](https://github.com/spf13/viper) for customizable config. The following config values may be set in a yaml file at `$HOME/.config/relay/config.yaml` or as environment variables with corresponding names in all caps, prefixed with `RELAY_`: 39 | 40 | - `debug`: Run Relay in debug mode. Overridden by global `--debug` flag. 41 | - `out=(text|json)`: Output mode. Overridden by global `--out` flag. 42 | - `yes`: Skip confirmation prompts. Overridden by global `--yes` flag. 43 | -------------------------------------------------------------------------------- /build/package/archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=puppet-relay 2 | pkgver=4.1.0 3 | pkgrel=1 4 | pkgdesc="CLI for Puppet's Relay workflow service" 5 | arch=('x86_64') 6 | url="https://github.com/puppetlabs/relay" 7 | license=('Apache-2.0') 8 | makedepends=('go') 9 | conflicts=('relay') 10 | source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") 11 | sha512sums=('8b57b37675d33852e7d7f6414887c1ce8859b133f1982cc5015755f6e30e38e7da756d9404910a0f2ba081b0481414589019143c1c064a354cc3e2522d29b513') 12 | 13 | build() { 14 | cd "relay-$pkgver" 15 | go build -mod=vendor -o relay ./cmd/relay 16 | } 17 | 18 | package() { 19 | cd "relay-$pkgver" 20 | install -Dm755 "relay" "$pkgdir/usr/bin/relay" 21 | install -Dm644 "LICENSE" "$pkgdir/usr/share/licenses/relay" 22 | install -Dm644 "README.md" "$pkgdir/usr/share/doc/relay/README.md" 23 | } 24 | -------------------------------------------------------------------------------- /build/package/brew/relay-cli-update-brew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | 3 | description: | 4 | This workflow issues a PR against puppetlabs/homebrew-puppet to update 5 | the version and sha when a new tag is cut on the puppetlabs/relay CLI. 6 | 7 | parameters: 8 | tag: 9 | description: version number of the new tagged binary 10 | sha: 11 | description: sha256 of the macos binary produced by the release build 12 | 13 | steps: 14 | - name: clone-and-edit-pr 15 | image: relaysh/core:latest 16 | spec: 17 | github_token: ${secrets.github-token} 18 | tag: ${parameters.tag} 19 | sha: ${parameters.sha} 20 | inputFile: https://raw.githubusercontent.com/puppetlabs/relay/main/build/package/brew/update_formula.sh 21 | -------------------------------------------------------------------------------- /build/package/brew/update_formula.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | GITHUB_TOKEN=$(ni get -p {.github_token}) 3 | TAG=$(ni get -p {.tag}) 4 | SHA=$(ni get -p {.sha}) 5 | if [[ $GITHUB_TOKEN =~ .{25,} ]] && [[ $TAG =~ ^v ]] && [[ $SHA =~ .{63,} ]] ; then 6 | git clone https://${GITHUB_TOKEN}@github.com/puppetlabs/homebrew-puppet 7 | cd homebrew-puppet 8 | git config user.name "Relay Autobot" && git config user.email "relay@users.noreply.github.com" 9 | PUBLISH_BRANCH=relay_${TAG} 10 | git checkout -b ${PUBLISH_BRANCH} 11 | sed -e "s/version \".*\"/version \"${TAG}\"/g" -i ./Formula/relay.rb 12 | sed -e "s/sha256 \".*\"/sha256 \"${SHA}\"/g" -i ./Formula/relay.rb 13 | COMMIT_MESSAGE="Update Relay to tagged version ${TAG}" 14 | git commit -am "${COMMIT_MESSAGE}" 15 | git push origin ${PUBLISH_BRANCH} 16 | PULLS_URI="https://api.github.com/repos/puppetlabs/homebrew-puppet/pulls" 17 | AUTH_HEADER="Authorization: token $GITHUB_TOKEN" 18 | NEW_PR_RESP=$(curl --data "{\"title\": \"${COMMIT_MESSAGE}\", \"head\": \"${PUBLISH_BRANCH}\", \"base\": \"main\"}" -X POST -s -H "${AUTH_HEADER}" ${PULLS_URI}) 19 | if [[ $? == 0 ]]; then 20 | PR_URL=$(echo $NEW_PR_RESP | jq ._links.html.href) 21 | ni output set --key result --value "Success! Pull request for $TAG submitted at: $PR_URL" 22 | else 23 | ni output set --key result --value "error submitting pull request: $NEW_PR_RESP" 24 | exit 1 25 | fi 26 | else 27 | ni output set --key result --value "bad input for one or more of: tag: [$TAG], sha: [$SHA], token: [sha256:$(echo $GITHUB_TOKEN | sha256sum)]" 28 | exit 1 29 | fi 30 | -------------------------------------------------------------------------------- /cmd/relay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/puppetlabs/relay/pkg/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /docs/generate.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | //go:generate go run ../cmd/relay doc generate -f relay.md 4 | -------------------------------------------------------------------------------- /docs/relay-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/relay.md: -------------------------------------------------------------------------------- 1 | ## relay 2 | 3 | Relay by Puppet 4 | 5 | ### Synopsis 6 | 7 | Relay connects your tools, APIs, and infrastructure 8 | to automate common tasks through simple, event-driven workflows. 9 | 10 | To get started, you'll need a relay.sh account - sign up for free 11 | by following this link: 🔗 https://relay.sh/ 12 | 13 | Once you've signed up, run this to log in: 14 | ▶️ relay auth login 15 | 16 | Use the 'workflow' subcommand to interact with workflows: 17 | ▶️ relay workflow 18 | 19 | 20 | ### Subcommand Usage 21 | 22 | **`relay auth login [flags]`** -- Log in to Relay 23 | ``` 24 | -f, --file string Read authentication credentials from file 25 | --stdin Read authentication credentials from stdin 26 | ``` 27 | 28 | **`relay auth logout`** -- Log out of Relay 29 | 30 | **`relay completion`** -- Generate shell completion scripts 31 | 32 | **`relay config auth clear [flags]`** -- Clear stored authentication data for the current context 33 | ``` 34 | -t, --type string Authentication type (api|session) 35 | ``` 36 | 37 | **`relay config global debug (true|false)`** -- Set global debug flag 38 | 39 | **`relay config global out (text|json)`** -- Set global out flag 40 | 41 | **`relay config global yes (true|false)`** -- Set global yes flag 42 | 43 | **`relay context set [context name]`** -- Set current context 44 | 45 | **`relay context view`** -- View current context 46 | 47 | **`relay dev initialize [flags]`** -- Initialize the Relay development environment 48 | ``` 49 | --install-helm-controller Optional installation of Helm Controller 50 | ``` 51 | 52 | **`relay dev metadata [flags]`** -- Run a mock metadata service 53 | 54 | This subcommand starts a mock metadata service which 55 | responds to queries from the Relay client SDKs, to help debug 56 | and test steps in your local environment. 57 | 58 | You can either run your code directly from this command by appending the 59 | invocation to the end of the command line. Or, without any arguments, 60 | it will start a persistent HTTP service bound to localhost which 61 | you can query repeatedly. 62 | ``` 63 | -i, --input string Path to metadata mock file 64 | -r, --run string Run ID of step to serve (default "1") 65 | -s, --step string Step name to serve (default "default") 66 | ``` 67 | 68 | **`relay dev workflow run [flags]`** -- Run a workflow on the dev cluster 69 | ``` 70 | -f, --file string Path to Relay workflow file 71 | -p, --parameter stringArray Parameters to invoke this workflow run with 72 | ``` 73 | 74 | **`relay dev workflow secret set [workflow name] [secret name] [flags]`** -- Set a workflow secret 75 | ``` 76 | --value-stdin accept secret value from stdin 77 | ``` 78 | 79 | **`relay doc generate [flags]`** -- Generate markdown documentation to stdout 80 | ``` 81 | -f, --file string The path to a file to write the documentation to 82 | ``` 83 | 84 | **`relay notifications clear read`** -- Clear all read notifications 85 | 86 | **`relay notifications list`** -- List notifications 87 | 88 | **`relay subscriptions list`** -- List workflow subscriptions 89 | 90 | **`relay subscriptions subscribe [workflow name]`** -- Subscribe to workflow 91 | 92 | **`relay subscriptions unsubscribe [workflow name]`** -- Unsubscribe to workflow 93 | 94 | **`relay tokens create [token name] [flags]`** -- Create API token 95 | ``` 96 | -f, --file string Write the generated token to the supplied file 97 | -u, --use Configure the CLI to use the generated API token (default true) 98 | ``` 99 | 100 | **`relay tokens list [flags]`** -- List API tokens 101 | ``` 102 | -a, --all Show all account tokens 103 | ``` 104 | 105 | **`relay tokens revoke [token id]`** -- Revoke API token 106 | 107 | **`relay version`** -- Print version 108 | 109 | **`relay workflow delete [workflow name]`** -- Delete a Relay workflow 110 | 111 | **`relay workflow download [workflow name] [flags]`** -- Download a workflow from the service 112 | ``` 113 | -f, --file string Path to write workflow file 114 | ``` 115 | 116 | **`relay workflow list`** -- Get a list of all your workflows 117 | 118 | **`relay workflow run [workflow name] [flags]`** -- Invoke a Relay workflow 119 | ``` 120 | -p, --parameter stringArray Parameters to invoke this workflow run with 121 | ``` 122 | 123 | **`relay workflow save [workflow name] [flags]`** -- Save a Relay workflow 124 | ``` 125 | -f, --file string Path to Relay workflow file 126 | -C, --no-create Do not create a workflow if it does not exist 127 | -O, --no-overwrite Do not overwrite an existing workflow 128 | ``` 129 | 130 | **`relay workflow secret delete [workflow name] [secret name]`** -- Delete a Relay workflow secret 131 | 132 | **`relay workflow secret list [workflow name]`** -- List Relay workflow secrets 133 | 134 | **`relay workflow secret set [workflow name] [secret name] [flags]`** -- Set a Relay workflow secret 135 | ``` 136 | --value-stdin accept secret value from stdin 137 | ``` 138 | 139 | **`relay workflow validate [flags]`** -- Validate a local Relay workflow file 140 | ``` 141 | -f, --file string Path to Relay workflow file 142 | ``` 143 | 144 | ### Global flags 145 | ``` 146 | -x, --context string Override the current context 147 | -d, --debug Print debugging information 148 | -h, --help Show help for this command 149 | -o, --out string Output type: (text|json) (default "text") 150 | -y, --yes Skip confirmation prompts 151 | 152 | ``` 153 | -------------------------------------------------------------------------------- /examples/metadata-configs/simple.yaml: -------------------------------------------------------------------------------- 1 | connections: 2 | aws/test: 3 | accessKeyID: AKIASAMPLEKEY 4 | secretAccessKey: 6bkpuV9fF3LX1Yo79OpfTwsw8wt5wsVLGTPJjDTu 5 | 6 | secrets: 7 | hello: world 8 | 9 | runs: 10 | '1234': 11 | steps: 12 | foo: 13 | spec: 14 | aws: !Connection [aws, test] 15 | foo: bar 16 | outputs: 17 | foo: bar 18 | state: 19 | foo: bar 20 | -------------------------------------------------------------------------------- /pkg/client/auth.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/puppetlabs/relay/pkg/errors" 8 | "github.com/puppetlabs/relay/pkg/model" 9 | ) 10 | 11 | type createTokenResponse struct { 12 | Token *model.Token `json:"token"` 13 | UserCode string `json:"user_code"` 14 | VerificationURI string `json:"verification_uri"` 15 | VerificationURIComplete string `json:"verification_uri_complete"` 16 | ExpiresAt time.Time `json:"expires_at"` 17 | } 18 | 19 | type UserDeviceValues struct { 20 | Token *model.Token 21 | UserCode string 22 | VerificationURI string 23 | VerificationURIComplete string 24 | } 25 | 26 | func (c *Client) CreateToken() (*UserDeviceValues, errors.Error) { 27 | response := &createTokenResponse{} 28 | if err := c.Request( 29 | WithMethod(http.MethodPost), 30 | WithPath("/auth/sessions/device"), 31 | WithResponseInto(response), 32 | ); err != nil { 33 | return nil, err 34 | } 35 | 36 | return &UserDeviceValues{ 37 | Token: response.Token, 38 | UserCode: response.UserCode, 39 | VerificationURI: response.VerificationURI, 40 | VerificationURIComplete: response.VerificationURIComplete, 41 | }, nil 42 | } 43 | 44 | func (c *Client) InvalidateToken() errors.Error { 45 | type deleteResponse struct { 46 | Success bool `json:"success"` 47 | } 48 | 49 | dr := &deleteResponse{} 50 | 51 | // Don't propagate error: if existing token is invalid endpoint will 401. Not sure this is 52 | // good behavior but it's true nonetheless 53 | c.Request( 54 | WithMethod(http.MethodDelete), 55 | WithPath("/auth/sessions"), 56 | WithResponseInto(dr), 57 | ) 58 | 59 | if err := c.clearToken(); err != nil { 60 | return errors.NewClientInternalError().WithCause(err) 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/puppetlabs/relay-client-go/client/pkg/client/openapi" 7 | "github.com/puppetlabs/relay/pkg/config" 8 | "github.com/puppetlabs/relay/pkg/model" 9 | ) 10 | 11 | const APIVersion = "v20200615" 12 | 13 | type Client struct { 14 | Api *openapi.APIClient 15 | 16 | config *config.Config 17 | httpClient *http.Client 18 | loadedToken *model.Token 19 | } 20 | 21 | func NewClient(config *config.Config) *Client { 22 | cc := openapi.NewConfiguration() 23 | if config.ContextConfig != nil { 24 | context := config.CurrentContext 25 | if contextConfig, ok := config.ContextConfig[context]; ok { 26 | if contextConfig.Domains != nil { 27 | cc.Host = contextConfig.Domains.APIDomain.Host 28 | cc.Scheme = contextConfig.Domains.APIDomain.Scheme 29 | } 30 | } 31 | } 32 | cc.Debug = false 33 | 34 | api := openapi.NewAPIClient(cc) 35 | 36 | httpClient := &http.Client{} 37 | var loadedToken *model.Token = nil 38 | 39 | return &Client{ 40 | api, 41 | config, 42 | httpClient, 43 | loadedToken, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/client/request.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | 13 | "github.com/puppetlabs/errawr-go/v2/pkg/encoding" 14 | "github.com/puppetlabs/relay/pkg/debug" 15 | "github.com/puppetlabs/relay/pkg/errors" 16 | ) 17 | 18 | type RequestOptions struct { 19 | method string 20 | path string 21 | headers map[string]string 22 | BodyEncodingType BodyEncodingType 23 | body interface{} 24 | responseBody interface{} 25 | } 26 | 27 | type RequestOptionSetter func(*RequestOptions) 28 | 29 | func WithMethod(method string) RequestOptionSetter { 30 | return func(opts *RequestOptions) { 31 | opts.method = method 32 | } 33 | } 34 | 35 | func WithPath(path string) RequestOptionSetter { 36 | return func(opts *RequestOptions) { 37 | opts.path = path 38 | } 39 | } 40 | 41 | func WithHeaders(headers map[string]string) RequestOptionSetter { 42 | return func(opts *RequestOptions) { 43 | opts.headers = headers 44 | } 45 | } 46 | 47 | func WithBody(body interface{}) RequestOptionSetter { 48 | return func(opts *RequestOptions) { 49 | opts.body = body 50 | } 51 | } 52 | 53 | func WithBodyEncodingType(bodyEncodingType BodyEncodingType) RequestOptionSetter { 54 | return func(opts *RequestOptions) { 55 | opts.BodyEncodingType = bodyEncodingType 56 | } 57 | } 58 | 59 | func WithResponseInto(responseBody interface{}) RequestOptionSetter { 60 | return func(opts *RequestOptions) { 61 | opts.responseBody = responseBody 62 | } 63 | } 64 | 65 | type BodyEncoding interface { 66 | ContentType() string 67 | Encode(interface{}) (io.ReadWriter, errors.Error) 68 | } 69 | 70 | type BodyEncodingType string 71 | 72 | const ( 73 | BodyEncodingTypeJSON BodyEncodingType = "json" 74 | BodyEncodingTypeYAML BodyEncodingType = "yaml" 75 | ) 76 | 77 | var mapEncodingTypeToEncoding = map[BodyEncodingType]BodyEncoding{ 78 | BodyEncodingTypeJSON: &JSONBodyEncoding{}, 79 | BodyEncodingTypeYAML: &YAMLBodyEncoding{}, 80 | } 81 | 82 | type JSONBodyEncoding struct{} 83 | 84 | func (j *JSONBodyEncoding) ContentType() string { 85 | return fmt.Sprintf("application/vnd.puppet.relay.%s+json", APIVersion) 86 | } 87 | 88 | func (j *JSONBodyEncoding) Encode(body interface{}) (io.ReadWriter, errors.Error) { 89 | var buf io.ReadWriter 90 | if body != nil { 91 | buf = new(bytes.Buffer) 92 | err := json.NewEncoder(buf).Encode(body) 93 | if err != nil { 94 | return nil, errors.NewClientInternalError().WithCause(err) 95 | } 96 | } 97 | 98 | return buf, nil 99 | } 100 | 101 | type YAMLBodyEncoding struct{} 102 | 103 | func (y *YAMLBodyEncoding) ContentType() string { 104 | return fmt.Sprintf("application/vnd.puppet.relay.%s+yaml", APIVersion) 105 | } 106 | 107 | func (y *YAMLBodyEncoding) Encode(body interface{}) (io.ReadWriter, errors.Error) { 108 | var buf io.ReadWriter 109 | 110 | bodyString, ok := body.(string) 111 | 112 | if !ok { 113 | return nil, errors.NewClientInternalError() 114 | } 115 | 116 | if body != nil { 117 | buf = bytes.NewBufferString(bodyString) 118 | } 119 | 120 | return buf, nil 121 | } 122 | 123 | func (c *Client) Request(setters ...RequestOptionSetter) errors.Error { 124 | const ( 125 | defaultMethod = http.MethodGet 126 | defaultBodyEncodingType = BodyEncodingTypeJSON 127 | ) 128 | 129 | opts := &RequestOptions{ 130 | method: defaultMethod, 131 | BodyEncodingType: defaultBodyEncodingType, 132 | } 133 | 134 | for _, setter := range setters { 135 | setter(opts) 136 | } 137 | 138 | contextConfig, ok := c.config.ContextConfig[c.config.CurrentContext] 139 | if !ok { 140 | return errors.NewClientInternalError() 141 | } 142 | 143 | if contextConfig.Domains == nil || contextConfig.Domains.APIDomain == nil { 144 | return errors.NewClientInternalError(). 145 | WithCause(errors.NewConfigInvalidAPIDomain("")) 146 | } 147 | 148 | rel := &url.URL{Path: opts.path} 149 | u := contextConfig.Domains.APIDomain.ResolveReference(rel) 150 | 151 | encoding, ok := mapEncodingTypeToEncoding[opts.BodyEncodingType] 152 | 153 | if !ok { 154 | encodingTypeError := errors.NewClientInvalidEncodingType(string(opts.BodyEncodingType)) 155 | 156 | return errors.NewClientInternalError().WithCause(encodingTypeError) 157 | } 158 | 159 | buf, buferr := encoding.Encode(opts.body) 160 | 161 | if buferr != nil { 162 | return buferr 163 | } 164 | 165 | req, reqerr := http.NewRequest(opts.method, u.String(), buf) 166 | 167 | if reqerr != nil { 168 | return errors.NewClientInternalError().WithCause(reqerr) 169 | } 170 | 171 | // defaults 172 | req.Header.Set("Accept", fmt.Sprintf("application/vnd.puppet.relay.%s+json", APIVersion)) 173 | 174 | if opts.body != nil { 175 | req.Header.Set("Content-Type", encoding.ContentType()) 176 | } 177 | 178 | // authorization 179 | token, terr := c.getToken() 180 | 181 | if terr != nil { 182 | return errors.NewClientInternalError().WithCause(terr) 183 | } 184 | 185 | if token != nil { 186 | req.Header.Set("Authorization", token.Bearer()) 187 | } 188 | 189 | // overrides 190 | for name, value := range opts.headers { 191 | req.Header.Set(name, value) 192 | } 193 | 194 | // temporary but very useful debugging solution until we get real logging in place 195 | debug.LogDump(httputil.DumpRequestOut(req, true)) 196 | 197 | resp, resperr := c.httpClient.Do(req) 198 | 199 | if resperr != nil { 200 | return errors.NewClientRequestError().WithCause(resperr) 201 | } 202 | 203 | // temporary but very useful debugging solution until we get real logging in place 204 | debug.LogDump(httputil.DumpResponse(resp, true)) 205 | 206 | defer resp.Body.Close() 207 | 208 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 209 | return parseError(resp) 210 | } 211 | 212 | if resp.Body != nil && opts.responseBody != nil { 213 | jerr := json.NewDecoder(resp.Body).Decode(opts.responseBody) 214 | 215 | if jerr != nil { 216 | return errors.NewClientInternalError().WithCause(jerr) 217 | } 218 | } 219 | 220 | return nil 221 | } 222 | 223 | func (c *Client) SetAuthorization() errors.Error { 224 | token, terr := c.getToken() 225 | 226 | if terr != nil { 227 | return errors.NewClientInternalError().WithCause(terr) 228 | } 229 | 230 | c.Api.GetConfig().AddDefaultHeader("Authorization", token.Bearer()) 231 | 232 | return nil 233 | } 234 | 235 | type errorEnvelope struct { 236 | Error *encoding.ErrorDisplayEnvelope `json:"error"` 237 | } 238 | 239 | func parseError(resp *http.Response) errors.Error { 240 | // read body to buffer 241 | bytes, berr := ioutil.ReadAll(resp.Body) 242 | 243 | if berr != nil { 244 | return errors.NewClientRequestError() 245 | } 246 | 247 | // Attempt to parse relay api error envelope containing an errawr 248 | var cause errors.Error 249 | env := &errorEnvelope{} 250 | if err := json.Unmarshal(bytes, env); err == nil { 251 | cause = env.Error.AsError() 252 | } else { 253 | cause = errors.NewClientBadRequestBody(string(bytes)) 254 | } 255 | 256 | // otherwise return generic errors based on response code 257 | switch resp.StatusCode { 258 | case http.StatusNotFound: 259 | return errors.NewClientResponseNotFound().WithCause(cause) 260 | case http.StatusUnauthorized: 261 | return errors.NewClientUserNotAuthenticated().WithCause(cause) 262 | case http.StatusForbidden: 263 | return errors.NewClientUserNotAuthorized().WithCause(cause) 264 | } 265 | 266 | return errors.NewClientRequestError().WithCause(cause) 267 | } 268 | -------------------------------------------------------------------------------- /pkg/client/revision.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "path" 8 | 9 | "github.com/puppetlabs/relay/pkg/errors" 10 | "github.com/puppetlabs/relay/pkg/model" 11 | ) 12 | 13 | func (c *Client) Validate(YAML string) (*model.RevisionEntity, errors.Error) { 14 | response := &model.RevisionEntity{} 15 | 16 | var headers = map[string]string{ 17 | "Content-Type": fmt.Sprintf("application/vnd.puppet.relay.%s+yaml", APIVersion), 18 | } 19 | 20 | if err := c.Request( 21 | WithMethod(http.MethodPost), 22 | WithPath(fmt.Sprintf("/api/revisions/validate")), 23 | WithBodyEncodingType(BodyEncodingTypeYAML), 24 | WithHeaders(headers), 25 | WithBody(YAML), 26 | WithResponseInto(response), 27 | ); err != nil { 28 | return nil, err 29 | } 30 | 31 | return response, nil 32 | } 33 | 34 | func (c *Client) CreateRevision(workflowName string, YAML string) (*model.RevisionEntity, errors.Error) { 35 | response := &model.RevisionEntity{} 36 | 37 | var headers = map[string]string{ 38 | "Content-Type": fmt.Sprintf("application/vnd.puppet.relay.%s+yaml", APIVersion), 39 | } 40 | 41 | if err := c.Request( 42 | WithMethod(http.MethodPost), 43 | WithPath(fmt.Sprintf("/api/workflows/%s/revisions", workflowName)), 44 | WithBodyEncodingType(BodyEncodingTypeYAML), 45 | WithHeaders(headers), 46 | WithBody(YAML), 47 | WithResponseInto(response), 48 | ); err != nil { 49 | return nil, err 50 | } 51 | 52 | return response, nil 53 | } 54 | 55 | func (c *Client) GetRevision(workflowName, revisionID string) (*model.RevisionEntity, errors.Error) { 56 | response := &model.RevisionEntity{} 57 | 58 | if err := c.Request( 59 | WithPath(path.Join("/api/workflows", url.PathEscape(workflowName), "revisions", url.PathEscape(revisionID))), 60 | WithResponseInto(response), 61 | ); err != nil { 62 | return nil, err 63 | } 64 | 65 | return response, nil 66 | } 67 | 68 | func (c *Client) GetLatestRevision(workflowName string) (*model.RevisionEntity, errors.Error) { 69 | wf, err := c.GetWorkflow(workflowName) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | if wf.Workflow.LatestRevision == nil || wf.Workflow.LatestRevision.ID == "" { 75 | return nil, errors.NewClientResponseNotFound() 76 | } 77 | 78 | return c.GetRevision(workflowName, wf.Workflow.LatestRevision.ID) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/client/token.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/puppetlabs/relay/pkg/config" 5 | "github.com/puppetlabs/relay/pkg/model" 6 | ) 7 | 8 | // getToken reads token from client cache or from path specified on config 9 | func (c *Client) getToken() (*model.Token, error) { 10 | if c.loadedToken == nil { 11 | if c.config.ContextConfig != nil { 12 | context := c.config.CurrentContext 13 | 14 | if contextConfig, ok := c.config.ContextConfig[context]; ok { 15 | if contextConfig.Auth != nil && contextConfig.Auth.Tokens != nil { 16 | for _, tokenType := range config.AuthTokenTypes() { 17 | if value, ok := contextConfig.Auth.Tokens[tokenType]; ok && value != "" { 18 | token := model.Token(value) 19 | c.loadedToken = &token 20 | return c.loadedToken, nil 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | return c.loadedToken, nil 29 | } 30 | 31 | // clearToken clears the token from the loadedToken cache on client object. 32 | func (c *Client) clearToken() error { 33 | c.loadedToken = nil 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/client/workflow.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/puppetlabs/leg/encoding/transfer" 10 | "github.com/puppetlabs/relay/pkg/debug" 11 | "github.com/puppetlabs/relay/pkg/errors" 12 | "github.com/puppetlabs/relay/pkg/model" 13 | ) 14 | 15 | type ListWorkflowSecretsResponse struct { 16 | WorkflowSecrets []model.WorkflowSecretSummary `json:"secrets"` 17 | } 18 | 19 | func (c *Client) ListWorkflowSecrets(workflow string) (*ListWorkflowSecretsResponse, errors.Error) { 20 | resp := &ListWorkflowSecretsResponse{} 21 | 22 | if err := c.Request( 23 | WithPath(fmt.Sprintf("/api/workflows/%v/secrets", workflow)), 24 | WithResponseInto(&resp)); err != nil { 25 | return nil, err 26 | } 27 | 28 | return resp, nil 29 | } 30 | 31 | type CreateWorkflowSecretParameters struct { 32 | Name string `json:"name"` 33 | Value transfer.JSONInterface `json:"value"` 34 | } 35 | 36 | func (c *Client) CreateWorkflowSecret(workflow, secret, value string) (*model.WorkflowSecretEntity, errors.Error) { 37 | params := &CreateWorkflowSecretParameters{ 38 | Name: secret, 39 | Value: transfer.JSONInterface{Data: value}, 40 | } 41 | 42 | response := &model.WorkflowSecretEntity{} 43 | 44 | if err := c.Request( 45 | WithMethod(http.MethodPost), 46 | WithPath(fmt.Sprintf("/api/workflows/%v/secrets", workflow)), 47 | WithBody(params), 48 | WithResponseInto(response), 49 | ); err != nil { 50 | return nil, err 51 | } 52 | 53 | return response, nil 54 | } 55 | 56 | type UpdateWorkflowSecretParameters struct { 57 | Value transfer.JSONInterface `json:"value"` 58 | } 59 | 60 | func (c *Client) UpdateWorkflowSecret(workflow, secret, value string) (*model.WorkflowSecretEntity, errors.Error) { 61 | params := &UpdateWorkflowSecretParameters{ 62 | Value: transfer.JSONInterface{Data: value}, 63 | } 64 | 65 | response := &model.WorkflowSecretEntity{} 66 | 67 | if err := c.Request( 68 | WithMethod(http.MethodPut), 69 | WithPath(fmt.Sprintf("/api/workflows/%v/secrets/%v", workflow, secret)), 70 | WithBody(params), 71 | WithResponseInto(response), 72 | ); err != nil { 73 | return nil, err 74 | } 75 | 76 | return response, nil 77 | } 78 | 79 | type DeleteWorkflowSecretResponse struct { 80 | Success bool `json:"success"` 81 | ResourceId string `json:"resource_id"` 82 | } 83 | 84 | func (c *Client) DeleteWorkflowSecret(workflow, secret string) (*DeleteWorkflowSecretResponse, errors.Error) { 85 | response := &DeleteWorkflowSecretResponse{} 86 | 87 | if err := c.Request( 88 | WithMethod(http.MethodDelete), 89 | WithPath(fmt.Sprintf("/api/workflows/%v/secrets/%v", workflow, secret)), 90 | WithResponseInto(response), 91 | ); err != nil { 92 | return nil, err 93 | } 94 | 95 | return response, nil 96 | } 97 | 98 | type CreateWorkflowParameters struct { 99 | Name string `json:"name"` 100 | Description string `json:"description"` 101 | } 102 | 103 | func (c *Client) CreateWorkflow(name string) (*model.WorkflowEntity, errors.Error) { 104 | params := &CreateWorkflowParameters{ 105 | Name: name, 106 | Description: "", 107 | } 108 | 109 | response := &model.WorkflowEntity{} 110 | 111 | if err := c.Request( 112 | WithMethod(http.MethodPost), 113 | WithPath("/api/workflows"), 114 | WithBody(params), 115 | WithResponseInto(response), 116 | ); err != nil { 117 | return nil, err 118 | } 119 | 120 | return response, nil 121 | } 122 | 123 | func (c *Client) GetWorkflow(name string) (*model.WorkflowEntity, errors.Error) { 124 | response := &model.WorkflowEntity{} 125 | 126 | if err := c.Request( 127 | WithPath(fmt.Sprintf("/api/workflows/%v", name)), 128 | WithResponseInto(response), 129 | ); err != nil { 130 | return nil, err 131 | } 132 | 133 | return response, nil 134 | } 135 | 136 | type DeleteWorkflowResponse struct { 137 | Success bool `json:"success"` 138 | ResourceId string `json:"resource_id"` 139 | } 140 | 141 | func (c *Client) DeleteWorkflow(name string) (*DeleteWorkflowResponse, errors.Error) { 142 | response := &DeleteWorkflowResponse{} 143 | 144 | if err := c.Request( 145 | WithMethod(http.MethodDelete), 146 | WithPath(fmt.Sprintf("/api/workflows/%v", name)), 147 | WithResponseInto(response), 148 | ); err != nil { 149 | return nil, err 150 | } 151 | 152 | return response, nil 153 | } 154 | 155 | type RunWorkflowParameterValueRequest struct { 156 | Value string `json:"value"` 157 | } 158 | 159 | type RunWorkflowRequest struct { 160 | Parameters map[string]RunWorkflowParameterValueRequest `json:"parameters"` 161 | } 162 | 163 | type RunWorkflowWorkflowResponse struct { 164 | Name string `json:"name"` 165 | } 166 | 167 | type RunWorkflowParameterValueResponse struct { 168 | Value string `json:"value"` 169 | } 170 | 171 | type RunWorkflowRevisionResponse struct { 172 | Id string `json:"id"` 173 | } 174 | 175 | type RunWorkflowStateResponse struct { 176 | Status string `json:"status"` 177 | StartedAt *time.Time `json:"started_at"` 178 | EndedAt *time.Time `json:"ended_at"` 179 | 180 | // TODO: Add steps here, in case we really care about that. 181 | } 182 | 183 | type RunWorkflowRunResponse struct { 184 | CreatedAt time.Time `json:"created_at"` 185 | RunNumber int `json:"run_number"` 186 | Revision RunWorkflowRevisionResponse `json:"revision"` 187 | State RunWorkflowStateResponse `json:"state"` 188 | Parameters map[string]RunWorkflowParameterValueResponse `json:"parameters"` 189 | Workflow RunWorkflowWorkflowResponse `json:"workflow"` 190 | } 191 | 192 | type RunWorkflowResponse struct { 193 | Run RunWorkflowRunResponse `json:"run"` 194 | } 195 | 196 | func setupParams(params map[string]string) map[string]RunWorkflowParameterValueRequest { 197 | res := make(map[string]RunWorkflowParameterValueRequest, len(params)) 198 | 199 | for key, val := range params { 200 | res[key] = RunWorkflowParameterValueRequest{val} 201 | } 202 | 203 | return res 204 | } 205 | 206 | func (c *Client) RunWorkflow(name string, params map[string]string) (*RunWorkflowResponse, errors.Error) { 207 | req := &RunWorkflowRequest{ 208 | Parameters: setupParams(params), 209 | } 210 | 211 | resp := &RunWorkflowResponse{} 212 | 213 | if err := c.Request( 214 | WithMethod(http.MethodPost), 215 | WithPath(fmt.Sprintf("/api/workflows/%v/runs", name)), 216 | WithBody(req), 217 | WithResponseInto(resp), 218 | ); err != nil { 219 | return nil, err 220 | } 221 | 222 | return resp, nil 223 | } 224 | 225 | // DownloadWorkflow gets the latest configuration (as a YAML string) for a 226 | // given workflow name. 227 | func (c *Client) DownloadWorkflow(name string) (string, errors.Error) { 228 | rev, err := c.GetLatestRevision(name) 229 | if err != nil { 230 | return "", err 231 | } 232 | 233 | dec, berr := base64.StdEncoding.DecodeString(rev.Revision.Raw) 234 | 235 | if berr != nil { 236 | debug.Logf("the workflow body was in the wrong format. %s", berr.Error()) 237 | return "", errors.NewClientUnknownError().WithCause(berr) 238 | } 239 | 240 | return string(dec), nil 241 | } 242 | -------------------------------------------------------------------------------- /pkg/cmd/auth.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | 8 | "github.com/cli/browser" 9 | "github.com/eiannone/keyboard" 10 | "github.com/puppetlabs/relay/pkg/config" 11 | "github.com/puppetlabs/relay/pkg/errors" 12 | "github.com/puppetlabs/relay/pkg/util" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // readLimit is set to 10kb to support RSA key files and the like. 17 | const readLimit = 10 * 1024 18 | 19 | func newAuthCommand() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "auth", 22 | Short: "Manage your authentication credentials", 23 | Args: cobra.MinimumNArgs(1), 24 | } 25 | 26 | cmd.AddCommand(newLoginCommand()) 27 | cmd.AddCommand(newLogoutCommand()) 28 | 29 | return cmd 30 | } 31 | 32 | func negotiateSession(cmd *cobra.Command) error { 33 | deviceValues, cterr := Client.CreateToken() 34 | if cterr != nil { 35 | return cterr 36 | } 37 | 38 | writeAuthTokenConfig(cmd, deviceValues.Token.String(), config.AuthTokenTypeSession) 39 | 40 | Dialog.Info("Stored authorization token.") 41 | 42 | Dialog.Info(fmt.Sprintf( 43 | `Your one-time code for activation is: 44 | 45 | **%s** 46 | * %s * 47 | **%s** 48 | 49 | Press [ENTER] to open %s in a browser or any other key to cancel...`, 50 | strings.Repeat("*", len(deviceValues.UserCode)), 51 | deviceValues.UserCode, 52 | strings.Repeat("*", len(deviceValues.UserCode)), 53 | deviceValues.VerificationURI, 54 | )) 55 | _, key, err := keyboard.GetSingleKey() 56 | if err != nil { 57 | return errors.NewGeneralUnknownError().WithCause(err) 58 | } 59 | 60 | if key != keyboard.KeyEnter { 61 | Dialog.Info("Canceled.") 62 | return nil 63 | } 64 | 65 | // The complete url may be empty, depending on the Device Auth Flow implementation. 66 | var uri string 67 | if deviceValues.VerificationURIComplete != "" { 68 | uri = deviceValues.VerificationURIComplete 69 | } else { 70 | uri = deviceValues.VerificationURI 71 | } 72 | if err := browser.OpenURL(uri); err != nil { 73 | return errors.NewAuthFailedLoginError().WithCause(fmt.Errorf("error opening the web browser: %w", err)) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func readAuthFromStdin(cmd *cobra.Command) error { 80 | gotStdin, err := util.PassedStdin() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if gotStdin { 86 | token, err := util.ReadStdin(readLimit) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | writeAuthTokenConfig(cmd, string(token), config.AuthTokenTypeAPI) 92 | } 93 | 94 | return nil 95 | } 96 | func doLogin(cmd *cobra.Command, args []string) error { 97 | Dialog.Progress("Getting authorization...") 98 | 99 | stdin, err := cmd.Flags().GetBool("stdin") 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if stdin { 105 | err = readAuthFromStdin(cmd) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | return nil 111 | } 112 | 113 | file, err := cmd.Flags().GetString("file") 114 | if err != nil { 115 | return nil 116 | } 117 | 118 | if file != "" { 119 | token, err := ioutil.ReadFile(file) 120 | if err != nil { 121 | return nil 122 | } 123 | 124 | writeAuthTokenConfig(cmd, string(token), config.AuthTokenTypeAPI) 125 | 126 | return nil 127 | } 128 | 129 | err = negotiateSession(cmd) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | Dialog.Info("Done!") 135 | return nil 136 | } 137 | 138 | func newLoginCommand() *cobra.Command { 139 | cmd := &cobra.Command{ 140 | Use: "login", 141 | Short: "Log in to Relay", 142 | Args: cobra.MaximumNArgs(1), 143 | RunE: doLogin, 144 | } 145 | 146 | cmd.Flags().StringP("file", "f", "", "Read authentication credentials from file") 147 | cmd.Flags().Bool("stdin", false, "Read authentication credentials from stdin") 148 | 149 | return cmd 150 | } 151 | 152 | func doLogout(cmd *cobra.Command, args []string) error { 153 | Dialog.Progress("Logging out...") 154 | 155 | iterr := Client.InvalidateToken() 156 | 157 | if iterr != nil { 158 | return iterr 159 | } 160 | 161 | Dialog.Info("You have been successfully logged out.") 162 | 163 | return nil 164 | } 165 | 166 | func newLogoutCommand() *cobra.Command { 167 | cmd := &cobra.Command{ 168 | Use: "logout", 169 | Short: "Log out of Relay", 170 | RunE: doLogout, 171 | } 172 | 173 | return cmd 174 | } 175 | 176 | func writeAuthTokenConfig(cmd *cobra.Command, token string, tokenType config.AuthTokenType) { 177 | if len(token) > 0 { 178 | cfg := &config.Config{ 179 | ContextConfig: map[string]*config.ContextConfig{ 180 | Config.CurrentContext: { 181 | Auth: &config.AuthConfig{ 182 | Tokens: map[config.AuthTokenType]string{ 183 | tokenType: strings.TrimSpace(token), 184 | }, 185 | }, 186 | }, 187 | }, 188 | } 189 | 190 | config.WriteConfig(cfg, cmd.Flags()) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /pkg/cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func newCompletionCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "completion", 12 | Short: "Generate shell completion scripts", 13 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 14 | Args: cobra.ExactValidArgs(1), 15 | Run: func(cmd *cobra.Command, args []string) { 16 | switch args[0] { 17 | case "bash": 18 | cmd.Root().GenBashCompletion(os.Stdout) 19 | case "zsh": 20 | cmd.Root().GenZshCompletion(os.Stdout) 21 | case "fish": 22 | cmd.Root().GenFishCompletion(os.Stdout, true) 23 | case "powershell": 24 | cmd.Root().GenPowerShellCompletion(os.Stdout) 25 | } 26 | }, 27 | } 28 | 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /pkg/cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/puppetlabs/relay/pkg/config" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newConfigCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "config", 15 | Short: "Manage Relay CLI configuration", 16 | Args: cobra.ExactArgs(0), 17 | } 18 | 19 | cmd.AddCommand(newConfigAuthCommand()) 20 | cmd.AddCommand(newConfigGlobalCommand()) 21 | 22 | return cmd 23 | } 24 | 25 | func newConfigAuthCommand() *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "auth", 28 | Short: "Manage Relay CLI authentication configuration", 29 | Args: cobra.ExactArgs(0), 30 | } 31 | 32 | cmd.AddCommand(newConfigAuthClearCommand()) 33 | 34 | return cmd 35 | } 36 | 37 | func newConfigAuthClearCommand() *cobra.Command { 38 | cmd := &cobra.Command{ 39 | Use: "clear", 40 | Short: "Clear stored authentication data for the current context", 41 | Args: cobra.ExactArgs(0), 42 | RunE: doConfigAuthClear, 43 | } 44 | 45 | cmd.Flags().StringP("type", "t", "", 46 | fmt.Sprintf("Authentication type (%s)", strings.Join(config.AuthTokenTypesAsString(), "|"))) 47 | 48 | return cmd 49 | } 50 | 51 | func doConfigAuthClear(cmd *cobra.Command, args []string) error { 52 | authTokenType, err := cmd.Flags().GetString("type") 53 | if err != nil { 54 | return err 55 | } 56 | 57 | context := Config.CurrentContext 58 | cfg := &config.Config{ 59 | ContextConfig: map[string]*config.ContextConfig{ 60 | context: { 61 | Auth: &config.AuthConfig{ 62 | Tokens: map[config.AuthTokenType]string{}, 63 | }, 64 | }, 65 | }, 66 | } 67 | 68 | for _, tokenType := range config.AuthTokenTypes() { 69 | if authTokenType == "" || authTokenType == tokenType.String() { 70 | cfg.ContextConfig[context].Auth.Tokens[tokenType] = "" 71 | } 72 | } 73 | 74 | config.WriteConfig(cfg, cmd.Flags()) 75 | 76 | return nil 77 | } 78 | 79 | func newConfigGlobalCommand() *cobra.Command { 80 | cmd := &cobra.Command{ 81 | Use: "global", 82 | Short: "Manage Relay CLI global options", 83 | Args: cobra.ExactArgs(0), 84 | } 85 | 86 | cmd.AddCommand(newConfigDebugFlagCommand()) 87 | cmd.AddCommand(newConfigOutFlagCommand()) 88 | cmd.AddCommand(newConfigYesFlagCommand()) 89 | 90 | return cmd 91 | } 92 | 93 | func newConfigDebugFlagCommand() *cobra.Command { 94 | cmd := &cobra.Command{ 95 | Use: "debug (true|false)", 96 | Short: "Set global debug flag", 97 | Args: cobra.ExactArgs(1), 98 | RunE: doConfigSetDebugFlag, 99 | } 100 | 101 | return cmd 102 | } 103 | 104 | func doConfigSetDebugFlag(cmd *cobra.Command, args []string) error { 105 | debug, err := strconv.ParseBool(args[0]) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | return config.WriteGlobalConfig(&config.Config{ 111 | Debug: debug, 112 | Out: Config.Out, 113 | Yes: Config.Yes, 114 | }, cmd.Flags()) 115 | } 116 | 117 | func newConfigOutFlagCommand() *cobra.Command { 118 | cmd := &cobra.Command{ 119 | Use: "out (text|json)", 120 | Short: "Set global out flag", 121 | Args: cobra.ExactArgs(1), 122 | RunE: doConfigSetOutFlag, 123 | } 124 | 125 | return cmd 126 | } 127 | func doConfigSetOutFlag(cmd *cobra.Command, args []string) error { 128 | switch args[0] { 129 | case config.OutputTypeJSON.String(): 130 | return config.WriteGlobalConfig(&config.Config{ 131 | Debug: Config.Debug, 132 | Out: config.OutputTypeJSON, 133 | Yes: Config.Yes, 134 | }, cmd.Flags()) 135 | case config.OutputTypeText.String(): 136 | return config.WriteGlobalConfig(&config.Config{ 137 | Debug: Config.Debug, 138 | Out: config.OutputTypeText, 139 | Yes: Config.Yes, 140 | }, cmd.Flags()) 141 | default: 142 | return fmt.Errorf("invalid output type: %s", args[0]) 143 | } 144 | } 145 | 146 | func newConfigYesFlagCommand() *cobra.Command { 147 | cmd := &cobra.Command{ 148 | Use: "yes (true|false)", 149 | Short: "Set global yes flag", 150 | Args: cobra.ExactArgs(1), 151 | RunE: doConfigSetYesFlag, 152 | } 153 | 154 | return cmd 155 | } 156 | 157 | func doConfigSetYesFlag(cmd *cobra.Command, args []string) error { 158 | yes, err := strconv.ParseBool(args[0]) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | return config.WriteGlobalConfig(&config.Config{ 164 | Debug: Config.Debug, 165 | Out: Config.Out, 166 | Yes: yes, 167 | }, cmd.Flags()) 168 | } 169 | -------------------------------------------------------------------------------- /pkg/cmd/context.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/puppetlabs/relay/pkg/config" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func newContextCommand() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "context", 11 | Short: "Manage Relay context", 12 | Args: cobra.ExactArgs(0), 13 | } 14 | 15 | cmd.AddCommand(newSetContext()) 16 | cmd.AddCommand(newViewContext()) 17 | 18 | return cmd 19 | } 20 | 21 | func newSetContext() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "set [context name]", 24 | Short: "Set current context", 25 | Args: cobra.ExactArgs(1), 26 | RunE: doSetContext, 27 | } 28 | 29 | return cmd 30 | } 31 | 32 | func newViewContext() *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "view", 35 | Short: "View current context", 36 | Args: cobra.ExactArgs(0), 37 | RunE: doViewContext, 38 | } 39 | 40 | return cmd 41 | } 42 | 43 | func doSetContext(cmd *cobra.Command, args []string) error { 44 | cfg := &config.Config{ 45 | CurrentContext: args[0], 46 | } 47 | 48 | config.WriteConfig(cfg, cmd.Flags()) 49 | 50 | return nil 51 | } 52 | 53 | func doViewContext(cmd *cobra.Command, args []string) error { 54 | context := Config.CurrentContext 55 | Dialog.Infof("Context: %s", context) 56 | 57 | if contextConfig, ok := Config.ContextConfig[context]; ok { 58 | if contextConfig.Domains != nil { 59 | Dialog.Infof("API Domain: %s", contextConfig.Domains.APIDomain) 60 | Dialog.Infof("UI Domain: %s", contextConfig.Domains.UIDomain) 61 | } else { 62 | Dialog.Info("No domains found for current context") 63 | } 64 | } else { 65 | Dialog.Info("No context configuration found") 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/cmd/dev.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/puppetlabs/leg/workdir" 8 | "github.com/puppetlabs/relay/pkg/config" 9 | "github.com/puppetlabs/relay/pkg/dev" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const ( 14 | InstallHelmControllerFlag = "install-helm-controller" 15 | ) 16 | 17 | var DevConfig = dev.Config{} 18 | 19 | func newDevCommand() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "dev", 22 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 23 | root := cmd.Root() 24 | 25 | err := root.PersistentPreRunE(cmd, args) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | datadir, err := workdir.NewNamespace([]string{"relay", "dev"}).New(workdir.DirTypeData, workdir.Options{}) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | DevConfig = dev.Config{ 36 | WorkDir: datadir, 37 | } 38 | return nil 39 | }, 40 | Short: "Manage the local development environment", 41 | Args: cobra.MinimumNArgs(1), 42 | } 43 | 44 | cmd.AddCommand(newInitializeCommand()) 45 | cmd.AddCommand(newMetadataCommand()) 46 | 47 | // TODO temporary workflow commands until `relay workflow` is integrated 48 | // with the dev cluster 49 | cmd.AddCommand(newDevWorkflowCommand()) 50 | 51 | return cmd 52 | } 53 | 54 | func newInitializeCommand() *cobra.Command { 55 | cmd := &cobra.Command{ 56 | Use: "initialize", 57 | Aliases: []string{"init"}, 58 | Short: "Initialize the Relay development environment", 59 | RunE: doInitDevelopmentEnvironment, 60 | } 61 | 62 | cmd.Flags().BoolP(InstallHelmControllerFlag, "", false, "Optional installation of Helm Controller") 63 | 64 | return cmd 65 | } 66 | 67 | func doInitDevelopmentEnvironment(cmd *cobra.Command, args []string) error { 68 | ctx := cmd.Context() 69 | 70 | installHelmController, err := cmd.Flags().GetBool(InstallHelmControllerFlag) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | opts := dev.InitializeOptions{ 76 | InstallHelmController: installHelmController, 77 | } 78 | 79 | return initDevelopmentEnvironment(ctx, opts) 80 | } 81 | 82 | func initDevelopmentEnvironment(ctx context.Context, initOpts dev.InitializeOptions) error { 83 | dm, err := dev.NewManager(ctx) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | installerOpts := mapInstallerOptionsFromConfig(Config.InstallerConfig, 89 | dev.InstallerOptions{ 90 | InstallerImage: dev.RelayInstallerImage, 91 | LogServiceImage: dev.RelayLogServiceImage, 92 | MetadataAPIImage: dev.RelayMetadataAPIImage, 93 | OperatorImage: dev.RelayOperatorImage, 94 | OperatorVaultInitImage: dev.RelayOperatorVaultInitImage, 95 | OperatorWebhookCertificateControllerImage: dev.RelayOperatorWebhookCertificateControllerImage, 96 | 97 | VaultServerImage: dev.DefaultVaultServerImage, 98 | VaultSidecarImage: dev.DefaultVaultSidecarImage, 99 | }) 100 | 101 | logServiceOpts := mapLogServiceOptionsFromConfig(Config.LogServiceConfig) 102 | 103 | Dialog.Info("Initializing relay-core; this may take several minutes...") 104 | 105 | if err := dm.InitializeRelayCore(ctx, initOpts, installerOpts, logServiceOpts); err != nil { 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // TODO the commands below are essentially duplicates of the primary workflow 113 | // and secret commands. These will eventually be merged with the main commands 114 | // after the experimental phase. 115 | 116 | func newDevWorkflowCommand() *cobra.Command { 117 | cmd := &cobra.Command{ 118 | Use: "workflow", 119 | Short: "Run Workflow commands against the dev cluster", 120 | } 121 | 122 | cmd.AddCommand(newDevWorkflowRunCommand()) 123 | cmd.AddCommand(newDevWorkflowSecretCommand()) 124 | 125 | return cmd 126 | } 127 | 128 | func newDevWorkflowRunCommand() *cobra.Command { 129 | cmd := &cobra.Command{ 130 | Use: "run", 131 | Short: "Run a workflow on the dev cluster", 132 | RunE: doDevWorkflowRun, 133 | } 134 | 135 | cmd.Flags().StringP("file", "f", "", "Path to Relay workflow file") 136 | cmd.MarkFlagRequired("file") 137 | 138 | cmd.Flags().StringArrayP("parameter", "p", []string{}, "Parameters to invoke this workflow run with") 139 | 140 | return cmd 141 | } 142 | 143 | func doDevWorkflowRun(cmd *cobra.Command, args []string) error { 144 | fp, err := cmd.Flags().GetString("file") 145 | if err != nil { 146 | return err 147 | } 148 | 149 | file, err := os.Open(fp) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | params, err := cmd.Flags().GetStringArray("parameter") 155 | if err != nil { 156 | return err 157 | } 158 | 159 | ctx := cmd.Context() 160 | dm, err := dev.NewManager(ctx) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | Dialog.Infof("Processing workflow file %s", fp) 166 | 167 | wd, err := dm.LoadWorkflow(ctx, file) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | t, err := dm.CreateTenant(ctx, wd.Name) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | wf, err := dm.CreateWorkflow(ctx, wd, t) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | _, err = dm.RunWorkflow(ctx, wf, parseParameters(params)) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | return nil 188 | } 189 | 190 | func newDevWorkflowSecretCommand() *cobra.Command { 191 | cmd := &cobra.Command{ 192 | Use: "secret", 193 | Short: "Manage workflow secrets", 194 | } 195 | 196 | cmd.AddCommand(newDevWorkflowSecretSetCommand()) 197 | 198 | return cmd 199 | } 200 | 201 | func newDevWorkflowSecretSetCommand() *cobra.Command { 202 | cmd := &cobra.Command{ 203 | Use: "set [workflow name] [secret name]", 204 | Short: "Set a workflow secret", 205 | Args: cobra.MaximumNArgs(2), 206 | RunE: doDevWorkflowSecretSet, 207 | } 208 | 209 | cmd.Flags().Bool("value-stdin", false, "accept secret value from stdin") 210 | 211 | return cmd 212 | } 213 | 214 | func doDevWorkflowSecretSet(cmd *cobra.Command, args []string) error { 215 | ctx := cmd.Context() 216 | 217 | dm, err := dev.NewManager(ctx) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | sc, err := getSecretValues(cmd, args) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | Dialog.Infof("Setting secret %s for workflow %s", sc.name, sc.workflowName) 228 | 229 | return dm.SetWorkflowSecret(ctx, sc.workflowName, sc.name, sc.value) 230 | } 231 | 232 | func mapInstallerOptionsFromConfig(installerConfig *config.InstallerConfig, defaultInstallerOpts dev.InstallerOptions) dev.InstallerOptions { 233 | installerOpts := defaultInstallerOpts 234 | if Config.InstallerConfig != nil { 235 | installerOpts.InstallerImage = coalesce(installerConfig.InstallerImage, defaultInstallerOpts.InstallerImage) 236 | installerOpts.LogServiceImage = coalesce(installerConfig.LogServiceImage, defaultInstallerOpts.LogServiceImage) 237 | installerOpts.MetadataAPIImage = coalesce(installerConfig.MetadataAPIImage, defaultInstallerOpts.MetadataAPIImage) 238 | installerOpts.OperatorImage = coalesce(installerConfig.OperatorImage, defaultInstallerOpts.OperatorImage) 239 | installerOpts.OperatorVaultInitImage = coalesce(installerConfig.OperatorVaultInitImage, defaultInstallerOpts.OperatorVaultInitImage) 240 | installerOpts.OperatorWebhookCertificateControllerImage = coalesce(installerConfig.OperatorWebhookCertificateControllerImage, defaultInstallerOpts.OperatorWebhookCertificateControllerImage) 241 | installerOpts.VaultServerImage = coalesce(installerConfig.VaultServerImage, defaultInstallerOpts.VaultServerImage) 242 | installerOpts.VaultSidecarImage = coalesce(installerConfig.VaultSidecarImage, defaultInstallerOpts.VaultSidecarImage) 243 | } 244 | 245 | return installerOpts 246 | } 247 | 248 | func mapLogServiceOptionsFromConfig(logServiceConfig *config.LogServiceConfig) dev.LogServiceOptions { 249 | logServiceOpts := dev.LogServiceOptions{} 250 | if logServiceConfig != nil { 251 | logServiceOpts = dev.LogServiceOptions{ 252 | CredentialsKey: logServiceConfig.CredentialsKey, 253 | CredentialsSecretName: logServiceConfig.CredentialsSecretName, 254 | Project: logServiceConfig.Project, 255 | Dataset: logServiceConfig.Dataset, 256 | Table: logServiceConfig.Table, 257 | } 258 | } 259 | 260 | return logServiceOpts 261 | } 262 | 263 | func coalesce(target string, other string) string { 264 | if target != "" { 265 | return target 266 | } 267 | 268 | return other 269 | } 270 | -------------------------------------------------------------------------------- /pkg/cmd/doc.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | 7 | "github.com/puppetlabs/relay/pkg/debug" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func newDocCommand() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "doc", 14 | Short: "generate docs for relay", 15 | Args: cobra.MinimumNArgs(1), 16 | } 17 | 18 | cmd.AddCommand(newGenerateCommand()) 19 | 20 | return cmd 21 | } 22 | 23 | func newGenerateCommand() *cobra.Command { 24 | cmd := &cobra.Command{ 25 | Use: "generate", 26 | Short: "Generate markdown documentation to stdout", 27 | Args: cobra.NoArgs, 28 | RunE: genDocs, 29 | } 30 | 31 | cmd.Flags().StringP("file", "f", "", "The path to a file to write the documentation to") 32 | 33 | return cmd 34 | } 35 | 36 | // genOverviewMarkdown makes a single-page 'man' style document 37 | // Much of this is copypasta from doc.GenMarkdownCustom, because 38 | // cobra/doc doesn't provide real formatting customization 39 | func genOverviewMarkdown() (md string, err error) { 40 | buf := new(bytes.Buffer) 41 | cmd := getCmd() 42 | 43 | // this is from doc.GenMarkdownCustom 44 | cmd.InitDefaultHelpCmd() 45 | cmd.InitDefaultHelpFlag() 46 | 47 | name := cmd.CommandPath() 48 | 49 | short := cmd.Short 50 | long := cmd.Long 51 | 52 | buf.WriteString("## " + name + "\n\n" + short + "\n\n") 53 | buf.WriteString("### Synopsis\n\n" + long + "\n\n") 54 | buf.WriteString("### Subcommand Usage\n\n") 55 | 56 | children := cmd.Commands() 57 | 58 | if err := testChildren(children, buf); err != nil { 59 | return buf.String(), err 60 | } 61 | 62 | buf.WriteString("### Global flags\n```\n") 63 | flags := cmd.PersistentFlags() 64 | buf.WriteString(flags.FlagUsages() + "\n```\n") 65 | 66 | markdown := buf.String() 67 | 68 | return markdown, err 69 | 70 | } 71 | 72 | // testChildren determines whether this command ought to be documented. 73 | // For brevity, we only want to generate docs for 'leaf' commands, i.e. 74 | // only "relay workflow add", not "relay workflow" 75 | func testChildren(children []*cobra.Command, buf *bytes.Buffer) error { 76 | 77 | for _, child := range children { 78 | if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { 79 | continue 80 | } 81 | if err := genChildMarkdown(child, buf); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | return nil 87 | 88 | } 89 | 90 | func genChildMarkdown(cmd *cobra.Command, buf *bytes.Buffer) error { 91 | if cmd.Runnable() { 92 | usage := cmd.UseLine() 93 | buf.WriteString("**`" + usage + "`** -- " + cmd.Short + "\n") 94 | long := cmd.Long 95 | if len(long) > 0 { 96 | buf.WriteString(" " + cmd.Long + "\n") 97 | } 98 | flags := cmd.NonInheritedFlags() 99 | if flags.HasAvailableFlags() { 100 | buf.WriteString("```\n") 101 | flags.SetOutput(buf) 102 | flags.PrintDefaults() 103 | buf.WriteString("```\n") 104 | } 105 | buf.WriteString("\n") 106 | } 107 | 108 | // Because commands can be nested arbitrarily deep, this recurses into 109 | // the current command's children and tests them for runnability 110 | children := cmd.Commands() 111 | testChildren(children, buf) 112 | 113 | return nil 114 | 115 | } 116 | 117 | func genDocs(cmd *cobra.Command, args []string) error { 118 | 119 | markdown, err := genOverviewMarkdown() 120 | if err != nil { 121 | Dialog.Errorf("problem generating markdown: %s", err.Error) 122 | } 123 | 124 | file, err := cmd.Flags().GetString("file") 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if file == "" { 130 | Dialog.WriteString(markdown) 131 | } else if err := ioutil.WriteFile(file, []byte(markdown), 0644); err != nil { 132 | debug.Logf("failed to write to file %s: %s", file, err.Error()) 133 | return err 134 | } 135 | 136 | return nil 137 | 138 | } 139 | -------------------------------------------------------------------------------- /pkg/cmd/main.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/puppetlabs/relay/pkg/client" 7 | "github.com/puppetlabs/relay/pkg/config" 8 | "github.com/puppetlabs/relay/pkg/debug" 9 | "github.com/puppetlabs/relay/pkg/dialog" 10 | "github.com/puppetlabs/relay/pkg/format" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // CommandName is the top level command that we're building 15 | const CommandName = "relay" 16 | 17 | // Config is the configuration that our commands should use. We can assume that 18 | // it's been configured accordingly by the time that a command executres. 19 | var Config = config.GetDefaultConfig() 20 | 21 | // Client is the client that we should use based on the configuration. If the 22 | // configuration can't be loaded then we can't assume that the client is 23 | // loaded. 24 | var Client = client.NewClient(Config) 25 | 26 | // Dialog is the UI to use derrived from the current configuration. 27 | var Dialog = dialog.FromConfig(Config) 28 | 29 | func getCmd() *cobra.Command { 30 | cmd := &cobra.Command{ 31 | Use: CommandName, 32 | Short: "Relay by Puppet", 33 | Args: cobra.MinimumNArgs(1), 34 | SilenceErrors: true, 35 | SilenceUsage: true, 36 | Long: `Relay connects your tools, APIs, and infrastructure 37 | to automate common tasks through simple, event-driven workflows. 38 | 39 | To get started, you'll need a relay.sh account - sign up for free 40 | by following this link: 🔗 https://relay.sh/ 41 | 42 | Once you've signed up, run this to log in: 43 | ▶️ relay auth login 44 | 45 | Use the 'workflow' subcommand to interact with workflows: 46 | ▶️ relay workflow 47 | `, 48 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 49 | // This turns off usage info in json output mode 50 | cfg, err := config.FromFlags(cmd.Flags()) 51 | 52 | if err != nil { 53 | // What kind of error could this be? We will abort accordingly. 54 | return err 55 | } else if err == nil && cfg.Out == config.OutputTypeJSON { 56 | cmd.SilenceUsage = true 57 | } 58 | 59 | // We have a config that we can assume is good to use. 60 | Config = cfg 61 | Client = client.NewClient(Config) 62 | Client.SetAuthorization() 63 | 64 | Dialog = dialog.FromConfig(Config) 65 | 66 | return nil 67 | }, 68 | } 69 | 70 | cmd.PersistentFlags().StringP("context", "x", "", "Override the current context") 71 | cmd.PersistentFlags().BoolVarP(&debug.Enabled, "debug", "d", false, "Print debugging information") 72 | cmd.PersistentFlags().BoolP("help", "h", false, "Show help for this command") 73 | cmd.PersistentFlags().BoolP("yes", "y", false, "Skip confirmation prompts") 74 | cmd.PersistentFlags().StringP("out", "o", "text", "Output type: (text|json)") 75 | 76 | // allow the user to override the default configuration location if they 77 | // can find the flag. likely figured out from reading this comment, actually... 78 | cmd.PersistentFlags().StringP("config", "c", "", "Path to config file (default is $HOME.config/relay)") 79 | cmd.PersistentFlags().MarkHidden("config") 80 | 81 | // Hide unwanted imported flags 82 | cmd.LocalFlags().MarkHidden("azure-container-registry-config") 83 | 84 | cmd.AddCommand(newAuthCommand()) 85 | cmd.AddCommand(newConfigCommand()) 86 | cmd.AddCommand(newContextCommand()) 87 | cmd.AddCommand(newWorkflowCommand()) 88 | cmd.AddCommand(newDevCommand()) 89 | cmd.AddCommand(newDocCommand()) 90 | cmd.AddCommand(newCompletionCommand()) 91 | cmd.AddCommand(newNotificationsCommand()) 92 | cmd.AddCommand(newSubscriptionsCommand()) 93 | cmd.AddCommand(newTokensCommand()) 94 | cmd.AddCommand(newVersionCommand()) 95 | 96 | return cmd 97 | } 98 | 99 | // Execute is here so the cmd builder can be called from the go test harness 100 | func Execute() { 101 | cmd := getCmd() 102 | 103 | if err := cmd.Execute(); err != nil { 104 | Dialog.Error(format.Error(err, cmd)) 105 | os.Exit(1) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pkg/cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/kballard/go-shellquote" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func parseArgs(str string) ([]string, error) { 14 | args, err := shellquote.Split(str) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | var filter []string 20 | 21 | for i, arg := range args { 22 | if i == 0 && arg == CommandName { 23 | continue 24 | } 25 | 26 | filter = append(filter, arg) 27 | } 28 | 29 | return filter, nil 30 | } 31 | 32 | func ExecuteCommand(args string) (string, string, error) { 33 | var stdout, stderr bytes.Buffer 34 | 35 | cmd := getCmd() 36 | cmd.SetOut(&stdout) 37 | cmd.SetErr(&stderr) 38 | pArgs, err := parseArgs(args) 39 | cmd.SetArgs(pArgs) 40 | if err != nil { 41 | return stdout.String(), stderr.String(), err 42 | } 43 | err = cmd.Execute() 44 | if err != nil { 45 | return stdout.String(), stderr.String(), err 46 | } 47 | 48 | return stdout.String(), stderr.String(), nil 49 | } 50 | 51 | func TestCommands(t *testing.T) { 52 | t.Run("`relay` should present help text", func(t *testing.T) { 53 | stdout, _, err := ExecuteCommand("relay") 54 | require.NoError(t, err) 55 | 56 | assert.True(t, strings.HasPrefix(stdout, "Relay connects your tools")) 57 | }) 58 | } 59 | 60 | func TestMetadataCommands(t *testing.T) { 61 | t.Run("`relay dev metadata` should present spec", func(t *testing.T) { 62 | t.Skip("Travis binds to ipv6 then fails because ipv6 isn't supported...") 63 | stdout, stderr, err := ExecuteCommand(`relay dev metadata --run 1234 --step foo --input ../../examples/metadata-configs/simple.yaml -- python -c "import os,requests; print(requests.get('{}/spec'.format(os.environ['METADATA_API_URL'])).content)"`) 64 | require.NoError(t, err) 65 | require.Empty(t, stderr) 66 | 67 | //TODO The go stderr and stdout are always empty and python's output goes to the console. Why is that? 68 | //assert.True(t, strings.HasPrefix(stdout, "6bkpuV9fF3LX1Yo79OpfTwsw8wt5wsVLGTPJjDTu")) 69 | require.Empty(t, stdout) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/cmd/metadata.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "time" 8 | 9 | "github.com/puppetlabs/relay/pkg/debug" 10 | "github.com/puppetlabs/relay/pkg/dev" 11 | "github.com/puppetlabs/relay/pkg/errors" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func newMetadataCommand() *cobra.Command { 16 | 17 | // TODO Add help about usage, idling and direct execution 18 | cmd := &cobra.Command{ 19 | Use: "metadata", 20 | Short: "Run a mock metadata service", 21 | Long: ` 22 | This subcommand starts a mock metadata service which 23 | responds to queries from the Relay client SDKs, to help debug 24 | and test steps in your local environment. 25 | 26 | You can either run your code directly from this command by appending the 27 | invocation to the end of the command line. Or, without any arguments, 28 | it will start a persistent HTTP service bound to localhost which 29 | you can query repeatedly.`, 30 | FParseErrWhitelist: cobra.FParseErrWhitelist{ 31 | UnknownFlags: true, 32 | }, 33 | RunE: doRunMetadata, 34 | } 35 | 36 | cmd.Flags().StringP("input", "i", "", "Path to metadata mock file") 37 | cmd.MarkFlagRequired("input") 38 | 39 | cmd.Flags().StringP("run", "r", "1", "Run ID of step to serve") 40 | 41 | cmd.Flags().StringP("step", "s", "default", "Step name to serve") 42 | 43 | return cmd 44 | } 45 | 46 | func doRunMetadata(cmd *cobra.Command, subcommand []string) error { 47 | input, err := cmd.Flags().GetString("input") 48 | if err != nil { 49 | debug.Log("The input flag is missing on the Cobra command configuration") 50 | return errors.NewGeneralUnknownError().WithCause(err).Bug() 51 | } 52 | runID, err := cmd.Flags().GetString("run") 53 | if err != nil { 54 | debug.Log("The run flag is missing on the Cobra command configuration") 55 | return errors.NewGeneralUnknownError().WithCause(err).Bug() 56 | } 57 | stepName, err := cmd.Flags().GetString("step") 58 | if err != nil { 59 | debug.Log("The step flag is missing on the Cobra command configuration") 60 | return errors.NewGeneralUnknownError().WithCause(err).Bug() 61 | } 62 | 63 | ctx := cmd.Context() 64 | m := dev.NewMetadataAPIManager(DevConfig) 65 | 66 | url, err := m.InitializeMetadataApi(ctx, dev.MetadataMockOptions{ 67 | RunID: runID, 68 | StepName: stepName, 69 | Input: input, 70 | }) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | if len(subcommand) == 0 { 76 | Dialog.Infof("No command was supplied, awaiting requests. Set environment with:\nexport METADATA_API_URL='%s'", url) 77 | for { 78 | time.Sleep(time.Second) 79 | } 80 | } else { 81 | // TODO this may not be strictly correct, if the subcommand is fancy 82 | command := exec.Command(subcommand[0], subcommand[1:]...) 83 | command.Env = os.Environ() 84 | command.Env = append(command.Env, fmt.Sprintf("METADATA_API_URL=%s", url)) 85 | command.Stdout = os.Stdout 86 | command.Stderr = os.Stderr 87 | 88 | err = command.Run() 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/cmd/notifications.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/puppetlabs/relay-client-go/client/pkg/client/openapi" 7 | "github.com/puppetlabs/relay/pkg/errors" 8 | "github.com/puppetlabs/relay/pkg/format" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newNotificationsCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "notifications", 15 | Short: "Manage your notifications", 16 | Args: cobra.MinimumNArgs(1), 17 | } 18 | 19 | cmd.AddCommand(newListUserNotificationsCommand()) 20 | cmd.AddCommand(newClearUserNotificationsCommand()) 21 | 22 | return cmd 23 | } 24 | 25 | func newListUserNotificationsCommand() *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "list", 28 | Short: "List notifications", 29 | Args: cobra.MaximumNArgs(1), 30 | RunE: doListUserNotifications, 31 | } 32 | 33 | return cmd 34 | } 35 | 36 | func newClearUserNotificationsCommand() *cobra.Command { 37 | cmd := &cobra.Command{ 38 | Use: "clear", 39 | Short: "Clear notifications", 40 | Args: cobra.MaximumNArgs(1), 41 | } 42 | 43 | cmd.AddCommand(newClearAllReadUserNotificationsCommand()) 44 | 45 | return cmd 46 | } 47 | 48 | func newClearAllReadUserNotificationsCommand() *cobra.Command { 49 | cmd := &cobra.Command{ 50 | Use: "read", 51 | Short: "Clear all read notifications", 52 | Args: cobra.MaximumNArgs(1), 53 | RunE: doClearAllReadUserNotifications, 54 | } 55 | 56 | return cmd 57 | } 58 | 59 | func doListUserNotifications(cmd *cobra.Command, args []string) error { 60 | Dialog.Progress("Listing notifications...") 61 | 62 | req := Client.Api.NotificationsApi.GetNotifications(cmd.Context()) 63 | n, _, err := Client.Api.NotificationsApi.GetNotificationsExecute(req) 64 | if err != nil { 65 | return errors.NewClientInternalError().WithCause(err) 66 | } 67 | 68 | if len(n.Notifications) == 0 { 69 | return nil 70 | } 71 | 72 | t := Dialog.Table() 73 | 74 | t.Headers([]string{"Status", "Type", "Name", "Run Number", "Link"}) 75 | 76 | for _, un := range n.Notifications { 77 | wfn := un.GetFields()["workflow_name"].(string) 78 | rn := int64(un.GetFields()["run_number"].(float64)) 79 | read := un.Read 80 | 81 | status := "" 82 | if !read { 83 | status = "NEW" 84 | } 85 | 86 | link := format.GuiLink(Config, "/workflows/%s/runs/%d/graph", wfn, rn) 87 | nt := "" 88 | switch un.Type { 89 | case "workflow.failed": 90 | nt = "Workflow failed" 91 | case "workflow.succeeded": 92 | nt = "Workflow succeeded" 93 | case "step.approval": 94 | nt = "Approval needed" 95 | } 96 | 97 | if nt != "" { 98 | t.AppendRow([]string{status, nt, wfn, fmt.Sprintf("%d", rn), link}) 99 | } 100 | } 101 | 102 | t.Flush() 103 | 104 | return nil 105 | } 106 | 107 | func doClearAllReadUserNotifications(cmd *cobra.Command, args []string) error { 108 | Dialog.Progress("Clearing notifications...") 109 | 110 | req := Client.Api.NotificationsApi.GetNotifications(cmd.Context()) 111 | n, _, err := Client.Api.NotificationsApi.GetNotificationsExecute(req) 112 | if err != nil { 113 | return errors.NewClientInternalError().WithCause(err) 114 | } 115 | 116 | nids := make([]string, 0) 117 | for _, un := range n.Notifications { 118 | if un.Read { 119 | nids = append(nids, un.GetId()) 120 | } 121 | } 122 | 123 | if len(nids) > 0 { 124 | req := Client.Api.NotificationsApi.PostAllNotificationDone(cmd.Context()) 125 | _, _, err := Client.Api.NotificationsApi.PostAllNotificationDoneExecute( 126 | req.NotificationIdentifiers(openapi.NotificationIdentifiers{Ids: nids}), 127 | ) 128 | if err != nil { 129 | return err 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/cmd/subscriptions.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/puppetlabs/relay-client-go/client/pkg/client/openapi" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func newSubscriptionsCommand() *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "subscriptions", 11 | Short: "Manage your Relay subscriptions", 12 | Args: cobra.MinimumNArgs(1), 13 | } 14 | 15 | cmd.AddCommand(newListUserWorkflowSubscriptions()) 16 | cmd.AddCommand(newSubscribeUserWorkflow()) 17 | cmd.AddCommand(newUnsubscribeUserWorkflow()) 18 | 19 | return cmd 20 | } 21 | 22 | func newListUserWorkflowSubscriptions() *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "list", 25 | Short: "List workflow subscriptions", 26 | Args: cobra.MaximumNArgs(1), 27 | RunE: doListUserWorkflowSubscriptions, 28 | } 29 | 30 | return cmd 31 | } 32 | 33 | func newSubscribeUserWorkflow() *cobra.Command { 34 | cmd := &cobra.Command{ 35 | Use: "subscribe [workflow name]", 36 | Short: "Subscribe to workflow", 37 | Args: cobra.MaximumNArgs(1), 38 | RunE: doSubscribeUserWorkflow, 39 | } 40 | 41 | return cmd 42 | } 43 | 44 | func newUnsubscribeUserWorkflow() *cobra.Command { 45 | cmd := &cobra.Command{ 46 | Use: "unsubscribe [workflow name]", 47 | Short: "Unsubscribe to workflow", 48 | Args: cobra.MaximumNArgs(1), 49 | RunE: doUnsubscribeUserWorkflow, 50 | } 51 | 52 | return cmd 53 | } 54 | 55 | func doListUserWorkflowSubscriptions(cmd *cobra.Command, args []string) error { 56 | Dialog.Progress("Listing workflow subscriptions...") 57 | 58 | req := Client.Api.SubscriptionsApi.GetWorkflowsSubscriptions(cmd.Context()) 59 | uws, _, err := Client.Api.SubscriptionsApi.GetWorkflowsSubscriptionsExecute(req) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | for _, wf := range uws.Workflows { 65 | if wf.Subscriptions != nil && 66 | *wf.Subscriptions.Subscribe { 67 | Dialog.Infof(wf.Name) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func doSubscribeUserWorkflow(cmd *cobra.Command, args []string) error { 75 | name, err := getWorkflowName(args) 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | Dialog.Progress("Subscribing...") 82 | 83 | var subscribe = true 84 | req := Client.Api.SubscriptionsApi.PutWorkflowSubscriptions(cmd.Context(), name) 85 | _, _, cerr := Client.Api.SubscriptionsApi.PutWorkflowSubscriptionsExecute( 86 | req.UserWorkflowSubscriptions( 87 | openapi.UserWorkflowSubscriptions{Subscribe: &subscribe})) 88 | if cerr != nil { 89 | return cerr 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func doUnsubscribeUserWorkflow(cmd *cobra.Command, args []string) error { 96 | name, err := getWorkflowName(args) 97 | 98 | if err != nil { 99 | return err 100 | } 101 | 102 | Dialog.Progress("Unsubscribing...") 103 | 104 | var subscribe = false 105 | req := Client.Api.SubscriptionsApi.PutWorkflowSubscriptions(cmd.Context(), name) 106 | _, _, cerr := Client.Api.SubscriptionsApi.PutWorkflowSubscriptionsExecute( 107 | req.UserWorkflowSubscriptions( 108 | openapi.UserWorkflowSubscriptions{Subscribe: &subscribe})) 109 | if cerr != nil { 110 | return cerr 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/cmd/token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/puppetlabs/relay-client-go/client/pkg/client/openapi" 9 | "github.com/puppetlabs/relay/pkg/config" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func newTokensCommand() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "tokens", 16 | Short: "Manage API tokens", 17 | Args: cobra.MinimumNArgs(1), 18 | } 19 | 20 | cmd.AddCommand(newCreateToken()) 21 | cmd.AddCommand(newListTokens()) 22 | cmd.AddCommand(newRevokeToken()) 23 | 24 | return cmd 25 | } 26 | 27 | func newCreateToken() *cobra.Command { 28 | cmd := &cobra.Command{ 29 | Use: "create [token name]", 30 | Short: "Create API token", 31 | Args: cobra.MinimumNArgs(1), 32 | RunE: doCreateToken, 33 | } 34 | 35 | cmd.Flags().BoolP("use", "u", true, "Configure the CLI to use the generated API token") 36 | cmd.Flags().StringP("file", "f", "", "Write the generated token to the supplied file") 37 | 38 | return cmd 39 | } 40 | 41 | func newRevokeToken() *cobra.Command { 42 | cmd := &cobra.Command{ 43 | Use: "revoke [token id]", 44 | Short: "Revoke API token", 45 | Args: cobra.MinimumNArgs(1), 46 | RunE: doRevokeToken, 47 | } 48 | 49 | return cmd 50 | } 51 | 52 | func newListTokens() *cobra.Command { 53 | cmd := &cobra.Command{ 54 | Use: "list", 55 | Short: "List API tokens", 56 | Args: cobra.MaximumNArgs(1), 57 | RunE: doListTokens, 58 | } 59 | 60 | cmd.Flags().BoolP("all", "a", false, "Show all account tokens") 61 | 62 | return cmd 63 | } 64 | 65 | func doCreateToken(cmd *cobra.Command, args []string) error { 66 | file, err := cmd.Flags().GetString("file") 67 | if err != nil { 68 | return err 69 | } 70 | 71 | var f *os.File 72 | if file != "" { 73 | f, err = os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | defer f.Close() 79 | } 80 | 81 | Dialog.Progress("Creating token...") 82 | 83 | req := Client.Api.TokensApi.CreateToken(cmd.Context()) 84 | t, resp, err := Client.Api.TokensApi.CreateTokenExecute( 85 | req.TokenRequest(openapi.TokenRequest{ 86 | Name: args[0], 87 | Type: "user", 88 | }), 89 | ) 90 | 91 | if err != nil { 92 | switch resp.StatusCode { 93 | case http.StatusConflict: 94 | // FIXME This is a bit of an assumption, but it is worth adding for overall usability. 95 | // A few things need to change to ensure an accurate error message is displayed. 96 | return errors.New("A token by that name already exists") 97 | default: 98 | return err 99 | } 100 | } 101 | 102 | if token, ok := t.GetTokenOk(); ok { 103 | secret := token.UserTokenWithSecret.GetSecret() 104 | if err != nil { 105 | return err 106 | } 107 | 108 | if file != "" { 109 | if _, err := f.Write([]byte(secret)); err != nil { 110 | return err 111 | } 112 | 113 | Dialog.Infof("Your token was written to %s\n"+ 114 | "Use this file to authenticate to the Relay CLI by running: relay auth login --file=%s\n", file, file) 115 | } 116 | 117 | use, err := cmd.Flags().GetBool("use") 118 | if err != nil { 119 | return err 120 | } 121 | 122 | if use { 123 | writeAuthTokenConfig(cmd, *token.UserTokenWithSecret.Secret, config.AuthTokenTypeAPI) 124 | 125 | Dialog.WriteString("The generated token has been added to the cached credentials\n" + 126 | "To clear your cached credentials, use: relay config auth clear\n") 127 | } 128 | 129 | if !use && file == "" { 130 | Dialog.WriteString(secret) 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func doRevokeToken(cmd *cobra.Command, args []string) error { 138 | Dialog.Progress("Revoking token...") 139 | 140 | req := Client.Api.TokensApi.DeleteToken(cmd.Context(), args[0]) 141 | _, _, err := Client.Api.TokensApi.DeleteTokenExecute(req) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func doListTokens(cmd *cobra.Command, args []string) error { 150 | Dialog.Progress("Listing tokens...") 151 | 152 | all, err := cmd.Flags().GetBool("all") 153 | if err != nil { 154 | return err 155 | } 156 | 157 | req := Client.Api.TokensApi.GetTokens(cmd.Context()) 158 | t, _, err := Client.Api.TokensApi.GetTokensExecute(req.Owned(!all).Valid(true)) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | if tokens, ok := t.GetTokensOk(); ok { 164 | t := Dialog.Table() 165 | 166 | t.Headers([]string{"User", "Id", "Name", "Type"}) 167 | 168 | for _, token := range tokens { 169 | t.AppendRow([]string{token.UserToken.GetUser().Name, token.UserToken.GetId(), token.UserToken.GetName(), token.UserToken.GetType()}) 170 | } 171 | 172 | t.Flush() 173 | } 174 | 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /pkg/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/puppetlabs/relay/pkg/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func newVersionCommand() *cobra.Command { 11 | return &cobra.Command{ 12 | Use: "version", 13 | Short: `Print version`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | v := version.GetVersion() 16 | if v == "" { 17 | fmt.Println("could not determine build information") 18 | } else { 19 | fmt.Println(v) 20 | } 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/cmd/workflow.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/puppetlabs/relay/pkg/debug" 11 | "github.com/puppetlabs/relay/pkg/errors" 12 | "github.com/puppetlabs/relay/pkg/format" 13 | "github.com/puppetlabs/relay/pkg/util" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func newWorkflowCommand() *cobra.Command { 18 | cmd := &cobra.Command{ 19 | Use: "workflow", 20 | Short: "Manage your Relay workflows", 21 | Args: cobra.MinimumNArgs(1), 22 | } 23 | 24 | cmd.AddCommand(newSaveWorkflowCommand()) 25 | cmd.AddCommand(newValidateWorkflowFileCommand()) 26 | cmd.AddCommand(newDeleteWorkflowCommand()) 27 | cmd.AddCommand(newRunWorkflowCommand()) 28 | cmd.AddCommand(newListWorkflowsCommand()) 29 | cmd.AddCommand(newDownloadWorkflowCommand()) 30 | cmd.AddCommand(newSecretCommand()) 31 | 32 | // Deprecated 33 | cmd.AddCommand(newAddWorkflowCommand()) 34 | cmd.AddCommand(newReplaceWorkflowCommand()) 35 | 36 | return cmd 37 | } 38 | 39 | func newSaveWorkflowCommand() *cobra.Command { 40 | cmd := &cobra.Command{ 41 | Use: "save [workflow name]", 42 | Short: "Save a Relay workflow", 43 | Args: cobra.MaximumNArgs(3), 44 | RunE: doSaveWorkflow, 45 | } 46 | 47 | cmd.Flags().StringP("file", "f", "", "Path to Relay workflow file") 48 | cmd.Flags().BoolP("no-overwrite", "O", false, "Do not overwrite an existing workflow") 49 | cmd.Flags().BoolP("no-create", "C", false, "Do not create a workflow if it does not exist") 50 | 51 | return cmd 52 | } 53 | 54 | func newAddWorkflowCommand() *cobra.Command { 55 | cmd := &cobra.Command{ 56 | Use: "add [workflow name]", 57 | Short: "Add a Relay workflow from a local file", 58 | Args: cobra.MaximumNArgs(1), 59 | RunE: doSaveWorkflow, 60 | Deprecated: "Use `save` instead", 61 | } 62 | 63 | cmd.Flags().StringP("file", "f", "", "Path to Relay workflow file") 64 | 65 | return cmd 66 | } 67 | 68 | func newReplaceWorkflowCommand() *cobra.Command { 69 | cmd := &cobra.Command{ 70 | Use: "replace [workflow name]", 71 | Short: "Replace an existing Relay workflow", 72 | Args: cobra.MaximumNArgs(1), 73 | RunE: doSaveWorkflow, 74 | Deprecated: "Use `save` instead", 75 | } 76 | 77 | cmd.Flags().StringP("file", "f", "", "Path to Relay workflow file") 78 | 79 | return cmd 80 | } 81 | 82 | func doValidateWorkflowFile(cmd *cobra.Command, args []string) error { 83 | filepath, file, err := readFile(cmd) 84 | 85 | if err != nil { 86 | return err 87 | } 88 | 89 | Dialog.Info("Validating workflow file " + filepath) 90 | 91 | _, rerr := Client.Validate(file) 92 | 93 | if rerr != nil { 94 | return rerr 95 | } 96 | 97 | Dialog.Infof(`Successfully validated workflow file %v`, filepath) 98 | 99 | return nil 100 | } 101 | 102 | func newValidateWorkflowFileCommand() *cobra.Command { 103 | cmd := &cobra.Command{ 104 | Use: "validate", 105 | Short: "Validate a local Relay workflow file", 106 | Args: cobra.MaximumNArgs(1), 107 | RunE: doValidateWorkflowFile, 108 | } 109 | 110 | cmd.Flags().StringP("file", "f", "", "Path to Relay workflow file") 111 | 112 | return cmd 113 | } 114 | 115 | func doDeleteWorkflow(cmd *cobra.Command, args []string) error { 116 | workflowName, nerr := getWorkflowName(args) 117 | 118 | if nerr != nil { 119 | return nerr 120 | } 121 | 122 | proceed, cerr := util.Confirm("Are you sure you want to delete this workflow?", Config) 123 | 124 | if cerr != nil { 125 | return cerr 126 | } 127 | 128 | if !proceed { 129 | return nil 130 | } 131 | 132 | Dialog.Progress("Deleting workflow...") 133 | 134 | _, err := Client.DeleteWorkflow(workflowName) 135 | 136 | if err != nil { 137 | return err 138 | } 139 | 140 | Dialog.Info("Workflow successfully deleted") 141 | 142 | return nil 143 | } 144 | 145 | func newDeleteWorkflowCommand() *cobra.Command { 146 | cmd := &cobra.Command{ 147 | Use: "delete [workflow name]", 148 | Short: "Delete a Relay workflow", 149 | Args: cobra.MaximumNArgs(1), 150 | RunE: doDeleteWorkflow, 151 | } 152 | 153 | return cmd 154 | } 155 | 156 | func parseParameter(str string) (key, value string) { 157 | strs := strings.SplitN(str, "=", 2) 158 | 159 | if len(strs) == 2 { 160 | return strs[0], strs[1] 161 | } 162 | 163 | debug.Logf("invalid parameter: %s", str) 164 | return "", "" 165 | } 166 | 167 | func parseParameters(strs []string) map[string]string { 168 | res := make(map[string]string) 169 | 170 | for _, str := range strs { 171 | key, val := parseParameter(str) 172 | 173 | // value of empty string could, indeed, be a valid parameter. 174 | if key != "" { 175 | res[key] = val 176 | } 177 | } 178 | 179 | return res 180 | } 181 | 182 | func doRunWorkflow(cmd *cobra.Command, args []string) error { 183 | params, err := cmd.Flags().GetStringArray("parameter") 184 | 185 | if err != nil { 186 | debug.Log("The parameters flag is missing on the Cobra command configuration") 187 | return errors.NewGeneralUnknownError().WithCause(err).Bug() 188 | } 189 | 190 | // TODO: Same here as above. Could really DRY all this up. 191 | name, err := getWorkflowName(args) 192 | 193 | if err != nil { 194 | return err 195 | } 196 | 197 | Dialog.Progress("Starting your workflow...") 198 | 199 | resp, err := Client.RunWorkflow(name, parseParameters(params)) 200 | 201 | if err != nil { 202 | return err 203 | } 204 | 205 | link := format.GuiLink(Config, "/workflows/%s/runs/%d/graph", name, resp.Run.RunNumber) 206 | Dialog.Info(fmt.Sprintf("Your run has started. Monitor its progress here: %s", link)) 207 | 208 | return nil 209 | } 210 | 211 | func newRunWorkflowCommand() *cobra.Command { 212 | cmd := &cobra.Command{ 213 | Use: "run [workflow name]", 214 | Short: "Invoke a Relay workflow", 215 | Args: cobra.MaximumNArgs(1), 216 | RunE: doRunWorkflow, 217 | ValidArgsFunction: doListWorkflowsCompletion, 218 | } 219 | 220 | cmd.Flags().StringArrayP("parameter", "p", []string{}, "Parameters to invoke this workflow run with") 221 | 222 | return cmd 223 | } 224 | 225 | func doDownloadWorkflow(cmd *cobra.Command, args []string) error { 226 | name, err := getWorkflowName(args) 227 | 228 | if err != nil { 229 | return err 230 | } 231 | 232 | body, err := Client.DownloadWorkflow(name) 233 | 234 | if err != nil { 235 | if errors.IsClientResponseNotFound(err) { 236 | Dialog.Warnf(`No file data found for workflow %v 237 | 238 | View more information or update workflow settings at: %v`, 239 | name, 240 | format.GuiLink(Config, "/workflows/%v", name), 241 | ) 242 | 243 | return nil 244 | } 245 | return err 246 | } 247 | 248 | filepath, ferr := cmd.Flags().GetString("file") 249 | if ferr != nil { 250 | return ferr 251 | } 252 | 253 | if filepath == "" { 254 | Dialog.WriteString(body) 255 | } else { 256 | if err := ioutil.WriteFile(filepath, []byte(body), 0644); err != nil { 257 | debug.Logf("failed to write to file %s: %s", filepath, err.Error()) 258 | return err 259 | } 260 | } 261 | 262 | return nil 263 | } 264 | 265 | func newDownloadWorkflowCommand() *cobra.Command { 266 | cmd := &cobra.Command{ 267 | Use: "download [workflow name]", 268 | Short: "Download a workflow from the service", 269 | Args: cobra.MaximumNArgs(1), 270 | RunE: doDownloadWorkflow, 271 | } 272 | 273 | cmd.Flags().StringP("file", "f", "", "Path to write workflow file") 274 | 275 | return cmd 276 | } 277 | 278 | func doListWorkflows(cmd *cobra.Command, args []string) error { 279 | req := Client.Api.ViewsApi.GetWorkflowsView(cmd.Context()) 280 | wv, _, err := Client.Api.ViewsApi.GetWorkflowsViewExecute(req) 281 | if err != nil { 282 | debug.Logf("failed to list workflows: %s", err.Error()) 283 | return err 284 | } 285 | 286 | t := Dialog.Table() 287 | 288 | t.Headers([]string{"Name", "Last Run Number"}) 289 | 290 | for _, workflow := range wv.Workflows { 291 | run := "" 292 | if workflow.MostRecentRun != nil { 293 | run = fmt.Sprintf("%d", workflow.MostRecentRun.RunNumber) 294 | } 295 | 296 | t.AppendRow([]string{workflow.Name, run}) 297 | } 298 | 299 | t.Flush() 300 | 301 | return nil 302 | } 303 | 304 | func doListWorkflowsCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 305 | if len(args) != 0 { 306 | return nil, cobra.ShellCompDirectiveNoFileComp 307 | } 308 | 309 | req := Client.Api.ViewsApi.GetWorkflowsView(cmd.Context()) 310 | wv, _, err := Client.Api.ViewsApi.GetWorkflowsViewExecute(req) 311 | if err != nil { 312 | return nil, cobra.ShellCompDirectiveNoFileComp 313 | } 314 | 315 | results := []string{} 316 | 317 | for _, workflow := range wv.Workflows { 318 | if strings.HasPrefix(workflow.Name, toComplete) { 319 | results = append(results, workflow.Name) 320 | } 321 | } 322 | 323 | return results, cobra.ShellCompDirectiveDefault 324 | } 325 | 326 | func newListWorkflowsCommand() *cobra.Command { 327 | cmd := &cobra.Command{ 328 | Use: "list", 329 | Short: "Get a list of all your workflows", 330 | Args: cobra.MaximumNArgs(0), 331 | RunE: doListWorkflows, 332 | } 333 | 334 | return cmd 335 | } 336 | 337 | // getWorkflowName gets the name of the workflow either from arguments or, if 338 | // none are supplied, reads it from stdin. 339 | func getWorkflowName(args []string) (string, errors.Error) { 340 | if len(args) > 0 { 341 | return args[0], nil 342 | } 343 | 344 | reader := bufio.NewReader(os.Stdin) 345 | 346 | fmt.Print("Workflow name: ") 347 | namePrompt, err := reader.ReadString('\n') 348 | 349 | if err != nil { 350 | return "", errors.NewWorkflowWorkflowNameReadError().WithCause(err) 351 | } 352 | 353 | name := strings.TrimSpace(namePrompt) 354 | 355 | if name == "" { 356 | return "", errors.NewWorkflowMissingNameError() 357 | } 358 | 359 | return strings.TrimSpace(namePrompt), nil 360 | } 361 | 362 | func readFile(cmd *cobra.Command) (string, string, errors.Error) { 363 | filepath, err := cmd.Flags().GetString("file") 364 | 365 | if err != nil { 366 | return "", "", errors.NewGeneralUnknownError().WithCause(err) 367 | } 368 | 369 | if filepath == "" { 370 | return "", "", errors.NewWorkflowMissingFileFlagError() 371 | } 372 | 373 | bytes, err := ioutil.ReadFile(filepath) 374 | 375 | if err != nil { 376 | return "", "", errors.NewWorkflowWorkflowFileReadError().WithCause(err) 377 | } 378 | 379 | return filepath, string(bytes), nil 380 | } 381 | -------------------------------------------------------------------------------- /pkg/cmd/workflow_save.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/puppetlabs/relay/pkg/errors" 7 | "github.com/puppetlabs/relay/pkg/format" 8 | "github.com/puppetlabs/relay/pkg/model" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func doSaveWorkflow(cmd *cobra.Command, args []string) error { 13 | workflowName, err := getWorkflowName(args) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | workflow, gerr := getOrCreateWorkflow(cmd, workflowName) 19 | if gerr != nil { 20 | return gerr 21 | } 22 | 23 | info := fmt.Sprintf("Successfully saved workflow %v.", workflow.Workflow.Name) 24 | 25 | if cmd.Flags().Changed("file") { 26 | info, err = updateWorkflowRevision(cmd, workflow) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | 32 | Dialog.Infof(`%s 33 | 34 | View more information or update workflow settings at: %v`, 35 | info, 36 | format.GuiLink(Config, "/workflows/%v", workflow.Workflow.Name), 37 | ) 38 | 39 | return nil 40 | } 41 | 42 | func getOrCreateWorkflow(cmd *cobra.Command, workflowName string) (*model.WorkflowEntity, error) { 43 | Dialog.Progress("Checking for workflow " + workflowName) 44 | 45 | workflow, err := Client.GetWorkflow(workflowName) 46 | if err != nil { 47 | if !errors.IsClientResponseNotFound(err) { 48 | return nil, err 49 | } 50 | 51 | if cmd.Name() == "replace" { 52 | return nil, errors.NewWorkflowDoesNotExistError() 53 | } 54 | 55 | if f := cmd.Flags().Lookup("no-create"); f != nil { 56 | if noCreate, err := cmd.Flags().GetBool("no-create"); err != nil { 57 | return nil, err 58 | } else if noCreate { 59 | return nil, errors.NewWorkflowDoesNotExistError() 60 | } 61 | } 62 | 63 | Dialog.Progress("Creating workflow " + workflowName) 64 | workflow, err = Client.CreateWorkflow(workflowName) 65 | if err != nil { 66 | return nil, err 67 | } 68 | } else { 69 | if cmd.Name() == "add" { 70 | return nil, errors.NewWorkflowAlreadyExistsError() 71 | } 72 | 73 | if f := cmd.Flags().Lookup("no-overwrite"); f != nil { 74 | if noOverwrite, err := cmd.Flags().GetBool("no-overwrite"); err != nil { 75 | return nil, err 76 | } else if noOverwrite { 77 | return nil, errors.NewWorkflowAlreadyExistsError() 78 | } 79 | } 80 | } 81 | 82 | return workflow, err 83 | } 84 | 85 | func updateWorkflowRevision(cmd *cobra.Command, workflow *model.WorkflowEntity) (string, errors.Error) { 86 | filePath, revisionContent, err := readFile(cmd) 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | info := fmt.Sprintf("Successfully saved workflow %v with file %s.", workflow.Workflow.Name, filePath) 92 | 93 | revision, err := Client.CreateRevision(workflow.Workflow.Name, revisionContent) 94 | if err != nil { 95 | return "", err 96 | } else { 97 | wr := model.NewWorkflowRevision(workflow.Workflow, revision.Revision) 98 | wr.Output(Config) 99 | } 100 | 101 | return info, nil 102 | } 103 | -------------------------------------------------------------------------------- /pkg/cmd/workflow_secret.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/puppetlabs/relay/pkg/debug" 13 | "github.com/puppetlabs/relay/pkg/errors" 14 | "github.com/puppetlabs/relay/pkg/format" 15 | "github.com/puppetlabs/relay/pkg/model" 16 | "github.com/puppetlabs/relay/pkg/util" 17 | "github.com/spf13/cobra" 18 | "golang.org/x/crypto/ssh/terminal" 19 | ) 20 | 21 | func newSecretCommand() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "secret", 24 | Short: "Manage your Relay secrets", 25 | Args: cobra.MinimumNArgs(1), 26 | } 27 | 28 | cmd.AddCommand(newSetSecretCommand()) 29 | cmd.AddCommand(newListSecretsCommand()) 30 | cmd.AddCommand(newDeleteSecretCommand()) 31 | 32 | return cmd 33 | } 34 | 35 | func newSetSecretCommand() *cobra.Command { 36 | cmd := &cobra.Command{ 37 | Use: "set [workflow name] [secret name]", 38 | Short: "Set a Relay workflow secret", 39 | Args: cobra.MaximumNArgs(2), 40 | RunE: doSetSecret, 41 | } 42 | 43 | cmd.Flags().Bool("value-stdin", false, "accept secret value from stdin") 44 | 45 | return cmd 46 | } 47 | 48 | func doSetSecret(cmd *cobra.Command, args []string) error { 49 | sc, err := getSecretValues(cmd, args) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | Dialog.Progress("Setting your secret...") 55 | 56 | resp, err := Client.ListWorkflowSecrets(sc.workflowName) 57 | if err != nil { 58 | debug.Logf("failed to list workflow secrets: %s", err.Error()) 59 | return err 60 | } 61 | 62 | exists := func() bool { 63 | for i := range resp.WorkflowSecrets { 64 | if resp.WorkflowSecrets[i].Name == sc.name { 65 | return true 66 | } 67 | } 68 | return false 69 | }() 70 | 71 | var secret *model.WorkflowSecretEntity 72 | if exists { 73 | secret, err = Client.UpdateWorkflowSecret(sc.workflowName, sc.name, sc.value) 74 | if err != nil { 75 | return err 76 | } 77 | } else { 78 | secret, err = Client.CreateWorkflowSecret(sc.workflowName, sc.name, sc.value) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | 84 | rev, err := Client.GetLatestRevision(sc.workflowName) 85 | if err != nil && !errors.IsClientResponseNotFound(err) { 86 | Dialog.Errorf(`Could not retrieve the latest revision for this workflow to check secret usage. 87 | 88 | %s`, format.Error(err, cmd)) 89 | } else if !secretUsed(rev, sc.name) { 90 | Dialog.Info(`🚩 This secret isn't used by your workflow yet. Don't forget to update your workflow code to use it!`) 91 | } 92 | 93 | Dialog.Infof(`Successfully set secret %v on workflow %v 94 | 95 | View more information or update workflow settings at: %v`, 96 | secret.Secret.Name, 97 | sc.workflowName, 98 | format.GuiLink(Config, "/workflows/%v", sc.workflowName), 99 | ) 100 | 101 | return nil 102 | } 103 | 104 | func newDeleteSecretCommand() *cobra.Command { 105 | cmd := &cobra.Command{ 106 | Use: "delete [workflow name] [secret name]", 107 | Short: "Delete a Relay workflow secret", 108 | Args: cobra.MaximumNArgs(2), 109 | RunE: doDeleteSecret, 110 | } 111 | 112 | return cmd 113 | } 114 | 115 | func doDeleteSecret(cmd *cobra.Command, args []string) error { 116 | workflowName, err := getWorkflowName(args) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | secretName, err := getSecretName(args) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | proceed, err := util.Confirm("Are you sure you want to delete this secret?", Config) 127 | if err != nil { 128 | return err 129 | } 130 | if !proceed { 131 | return nil 132 | } 133 | 134 | Dialog.Progress("Deleting secret...") 135 | _, err = Client.DeleteWorkflowSecret(workflowName, secretName) 136 | if err != nil { 137 | return err 138 | } 139 | Dialog.Info("Secret successfully deleted") 140 | 141 | return nil 142 | } 143 | 144 | func newListSecretsCommand() *cobra.Command { 145 | cmd := &cobra.Command{ 146 | Use: "list [workflow name]", 147 | Short: "List Relay workflow secrets", 148 | Args: cobra.MaximumNArgs(1), 149 | RunE: doListSecrets, 150 | } 151 | 152 | return cmd 153 | } 154 | 155 | func doListSecrets(cmd *cobra.Command, args []string) error { 156 | workflowName, err := getWorkflowName(args) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | resp, err := Client.ListWorkflowSecrets(workflowName) 162 | if err != nil { 163 | debug.Logf("failed to list workflow secrets: %s", err.Error()) 164 | return err 165 | } 166 | 167 | t := Dialog.Table() 168 | 169 | t.Headers([]string{"Name"}) 170 | 171 | for _, secret := range resp.WorkflowSecrets { 172 | t.AppendRow([]string{secret.Name}) 173 | } 174 | 175 | t.Flush() 176 | 177 | return nil 178 | 179 | } 180 | 181 | type secretValues struct { 182 | workflowName string 183 | name string 184 | value string 185 | } 186 | 187 | func getSecretValues(cmd *cobra.Command, args []string) (*secretValues, errors.Error) { 188 | workflowName, err := getWorkflowName(args) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | secretName, err := getSecretName(args) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | secretValue, err := getSecretValue(cmd) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | return &secretValues{ 204 | workflowName: workflowName, 205 | name: secretName, 206 | value: secretValue, 207 | }, nil 208 | } 209 | 210 | // getSecretName gets the name of the secret from the second argument. If 211 | // none are supplied, reads it from stdin. 212 | func getSecretName(args []string) (string, errors.Error) { 213 | if len(args) > 1 { 214 | return args[1], nil 215 | } 216 | 217 | reader := bufio.NewReader(os.Stdin) 218 | 219 | fmt.Print("Secret name: ") 220 | namePrompt, err := reader.ReadString('\n') 221 | if err != nil { 222 | return "", errors.NewSecretNameReadError().WithCause(err) 223 | } 224 | 225 | name := strings.TrimSpace(namePrompt) 226 | 227 | if name == "" { 228 | return "", errors.NewSecretMissingNameError() 229 | } 230 | 231 | return strings.TrimSpace(namePrompt), nil 232 | } 233 | 234 | // getSecretValue either prompts for the value of the secret with hidden input, or accepts the value from stdin if the 235 | // --value-stdin boolean flag is set 236 | func getSecretValue(cmd *cobra.Command) (string, errors.Error) { 237 | var value string 238 | 239 | valueFromStdin, err := cmd.Flags().GetBool("value-stdin") 240 | if err != nil { 241 | return "", errors.NewGeneralUnknownError().WithCause(err) 242 | } 243 | 244 | if valueFromStdin { 245 | gotStdin, err := util.PassedStdin() 246 | if err != nil { 247 | return "", errors.NewSecretFailedValueFromStdin().WithCause(err) 248 | } 249 | 250 | if gotStdin { 251 | buf := bytes.Buffer{} 252 | reader := &io.LimitedReader{R: os.Stdin, N: readLimit} 253 | 254 | n, err := buf.ReadFrom(reader) 255 | if err != nil && err != io.EOF { 256 | return "", errors.NewSecretFailedValueFromStdin().WithCause(err) 257 | } 258 | if n == 0 { 259 | return "", errors.NewSecretFailedNoStdin() 260 | } 261 | 262 | value = buf.String() 263 | } else { 264 | return "", errors.NewSecretFailedNoStdin() 265 | } 266 | } else { 267 | fmt.Print("Value: ") 268 | valueBytes, err := terminal.ReadPassword(int(syscall.Stdin)) 269 | if err != nil { 270 | return "", errors.NewSecretFailedValueFromStdin().WithCause(err) 271 | } 272 | 273 | value = string(valueBytes) 274 | // resets to new line after hidden input 275 | fmt.Println("") 276 | } 277 | 278 | return value, nil 279 | } 280 | 281 | func secretUsed(rev *model.RevisionEntity, name string) bool { 282 | if rev == nil || rev.Revision == nil { 283 | return false 284 | } 285 | 286 | for _, step := range rev.Revision.Steps { 287 | if step.References == nil { 288 | // Possibly non-container step type. 289 | continue 290 | } 291 | 292 | for _, secret := range step.References.Secrets { 293 | if secret.Name == name { 294 | return true 295 | } 296 | } 297 | } 298 | 299 | return false 300 | } 301 | -------------------------------------------------------------------------------- /pkg/debug/debug.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import "log" 4 | 5 | // Enabling this will enable debug logging 6 | var Enabled = false 7 | 8 | func Log(msg string) { 9 | if Enabled { 10 | log.Printf(msg) 11 | } 12 | } 13 | 14 | func Logf(msg string, args ...interface{}) { 15 | if Enabled { 16 | log.Printf(msg, args...) 17 | } 18 | } 19 | 20 | func LogDump(msg []byte, err error) { 21 | if Enabled { 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | log.Printf(string(msg)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/dev/admin.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "path" 7 | 8 | "github.com/puppetlabs/leg/timeutil/pkg/retry" 9 | corev1 "k8s.io/api/core/v1" 10 | rbacv1 "k8s.io/api/rbac/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | const ( 17 | relayAdminServiceAccountName = "relay-admin-user" 18 | relayClusterConnectionID = "_relay-dev-cluster" 19 | 20 | RelayClusterConnectionName = "relay-dev-cluster" 21 | ) 22 | 23 | type adminObjects struct { 24 | serviceAccount corev1.ServiceAccount 25 | secret corev1.Secret 26 | clusterRoleBinding rbacv1.ClusterRoleBinding 27 | } 28 | 29 | func newAdminObjects() *adminObjects { 30 | objectMeta := metav1.ObjectMeta{ 31 | Name: relayAdminServiceAccountName, 32 | Namespace: systemNamespace, 33 | } 34 | 35 | return &adminObjects{ 36 | serviceAccount: corev1.ServiceAccount{ObjectMeta: objectMeta}, 37 | secret: corev1.Secret{ObjectMeta: objectMeta}, 38 | clusterRoleBinding: rbacv1.ClusterRoleBinding{ 39 | ObjectMeta: metav1.ObjectMeta{ 40 | Name: objectMeta.Name, 41 | }, 42 | }, 43 | } 44 | } 45 | 46 | type adminManager struct { 47 | cl *Client 48 | objects *adminObjects 49 | vm *vaultManager 50 | } 51 | 52 | func (m *adminManager) reconcile(ctx context.Context) error { 53 | if _, err := ctrl.CreateOrUpdate(ctx, m.cl.APIClient, &m.objects.serviceAccount, func() error { 54 | return nil 55 | }); err != nil { 56 | return err 57 | } 58 | 59 | if _, err := ctrl.CreateOrUpdate(ctx, m.cl.APIClient, &m.objects.secret, func() error { 60 | m.secret(&m.objects.secret) 61 | 62 | return nil 63 | }); err != nil { 64 | return err 65 | } 66 | 67 | if _, err := ctrl.CreateOrUpdate(ctx, m.cl.APIClient, &m.objects.clusterRoleBinding, func() error { 68 | m.clusterRoleBinding(&m.objects.clusterRoleBinding) 69 | 70 | return nil 71 | }); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (m *adminManager) secret(secret *corev1.Secret) { 79 | if secret.Annotations == nil { 80 | secret.Annotations = make(map[string]string) 81 | } 82 | 83 | secret.Annotations["kubernetes.io/service-account.name"] = m.objects.serviceAccount.Name 84 | secret.Type = corev1.SecretTypeServiceAccountToken 85 | } 86 | 87 | func (m *adminManager) clusterRoleBinding(clusterRoleBinding *rbacv1.ClusterRoleBinding) { 88 | clusterRoleBinding.RoleRef = rbacv1.RoleRef{ 89 | APIGroup: "rbac.authorization.k8s.io", 90 | Kind: "ClusterRole", 91 | Name: "cluster-admin", 92 | } 93 | 94 | clusterRoleBinding.Subjects = []rbacv1.Subject{ 95 | { 96 | Kind: "ServiceAccount", 97 | Name: m.objects.serviceAccount.Name, 98 | Namespace: m.objects.serviceAccount.Namespace, 99 | }, 100 | } 101 | } 102 | 103 | func (m *adminManager) addConnectionForWorkflow(ctx context.Context, name string) error { 104 | secretKey := client.ObjectKeyFromObject(&m.objects.secret) 105 | 106 | err := retry.Wait(ctx, func(ctx context.Context) (bool, error) { 107 | if err := m.cl.APIClient.Get(ctx, secretKey, &m.objects.secret); err != nil { 108 | return retry.Repeat(err) 109 | } 110 | 111 | if len(m.objects.secret.Data) == 0 { 112 | return retry.Repeat(errors.New("secret data is empty")) 113 | } 114 | 115 | return retry.Done(nil) 116 | }) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | data := m.objects.secret.Data 122 | 123 | connectionsPath := path.Join("customers", "connections", name) 124 | pointerPath := path.Join(connectionsPath, "kubernetes", RelayClusterConnectionName) 125 | base := path.Join(connectionsPath, relayClusterConnectionID) 126 | 127 | connectionSecrets := map[string]string{ 128 | pointerPath: relayClusterConnectionID, 129 | path.Join(base, "token"): string(data["token"]), 130 | path.Join(base, "certificateAuthority"): string(data["ca.crt"]), 131 | path.Join(base, "server"): "https://kubernetes.default.svc.cluster.local", 132 | } 133 | 134 | return m.vm.writeSecrets(ctx, connectionSecrets) 135 | } 136 | 137 | func newAdminManager(cl *Client, vm *vaultManager) *adminManager { 138 | return &adminManager{ 139 | cl: cl, 140 | objects: newAdminObjects(), 141 | vm: vm, 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /pkg/dev/dev.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "path" 7 | "time" 8 | 9 | "github.com/puppetlabs/leg/workdir" 10 | v1 "github.com/puppetlabs/relay-client-go/models/pkg/workflow/types/v1" 11 | installerv1alpha1 "github.com/puppetlabs/relay-core/pkg/apis/install.relay.sh/v1alpha1" 12 | relayv1beta1 "github.com/puppetlabs/relay-core/pkg/apis/relay.sh/v1beta1" 13 | "github.com/puppetlabs/relay-core/pkg/obj" 14 | "github.com/puppetlabs/relay-core/pkg/operator/dependency" 15 | helmchartv1 "github.com/rancher/helm-controller/pkg/apis/helm.cattle.io/v1" 16 | rbacv1 "k8s.io/api/rbac/v1" 17 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 18 | apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 19 | apierrors "k8s.io/apimachinery/pkg/api/errors" 20 | "k8s.io/apimachinery/pkg/api/meta" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apiserver/pkg/storage/names" 24 | kubernetesscheme "k8s.io/client-go/kubernetes/scheme" 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | "k8s.io/client-go/tools/clientcmd" 27 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 28 | cachingv1alpha1 "knative.dev/caching/pkg/apis/caching/v1alpha1" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 31 | ) 32 | 33 | var ( 34 | DefaultScheme = runtime.NewScheme() 35 | schemeBuilder = runtime.NewSchemeBuilder( 36 | kubernetesscheme.AddToScheme, 37 | metav1.AddMetaToScheme, 38 | rbacv1.AddToScheme, 39 | apiextensionsv1.AddToScheme, 40 | apiextensionsv1beta1.AddToScheme, 41 | dependency.AddToScheme, 42 | helmchartv1.AddToScheme, 43 | cachingv1alpha1.AddToScheme, 44 | installerv1alpha1.AddToScheme, 45 | ) 46 | _ = schemeBuilder.AddToScheme(DefaultScheme) 47 | ) 48 | 49 | const ( 50 | defaultWorkflowName = "relay-workflow" 51 | jwtSigningKeysSecretName = "relay-core-v1-operator-signing-keys" 52 | 53 | VaultEngineMountCustomers = "customers" 54 | VaultEngineMountWorkflows = "workflows" 55 | ) 56 | 57 | type Client struct { 58 | APIClient client.Client 59 | Mapper meta.RESTMapper 60 | } 61 | 62 | type ClientOptions struct { 63 | Scheme *runtime.Scheme 64 | } 65 | 66 | type Config struct { 67 | WorkDir *workdir.WorkDir 68 | } 69 | 70 | type Manager struct { 71 | cl *Client 72 | cfg Config 73 | } 74 | 75 | type InitializeOptions struct { 76 | InstallHelmController bool 77 | } 78 | 79 | type InstallerOptions struct { 80 | InstallerImage string 81 | LogServiceImage string 82 | MetadataAPIImage string 83 | OperatorImage string 84 | OperatorVaultInitImage string 85 | OperatorWebhookCertificateControllerImage string 86 | VaultServerImage string 87 | VaultSidecarImage string 88 | } 89 | 90 | // FIXME Consider a better mechanism for specific service options 91 | type LogServiceOptions struct { 92 | CredentialsKey string 93 | CredentialsSecretName string 94 | Project string 95 | Dataset string 96 | Table string 97 | } 98 | 99 | func (m *Manager) LoadWorkflow(ctx context.Context, r io.ReadCloser) (*v1.WorkflowData, error) { 100 | decoder := v1.NewDocumentStreamingDecoder(r, &v1.YAMLDecoder{}) 101 | 102 | wd, err := decoder.DecodeStream(ctx) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return wd, nil 108 | } 109 | 110 | func (m *Manager) CreateTenant(ctx context.Context, name string) (*relayv1beta1.Tenant, error) { 111 | mapper := v1.NewDefaultTenantEngineMapper( 112 | v1.WithNameTenantOption(name), 113 | v1.WithIDTenantOption(name), 114 | v1.WithWorkflowNameTenantOption(name), 115 | v1.WithWorkflowIDTenantOption(name), 116 | v1.WithNamespaceTenantOption(tenantNamespace), 117 | ) 118 | 119 | mapping, err := mapper.ToRuntimeObjectsManifest() 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | if err := m.cl.APIClient.Create(ctx, mapping.Namespace); err != nil { 125 | if !apierrors.IsAlreadyExists(err) { 126 | return nil, err 127 | } 128 | } 129 | 130 | key := client.ObjectKey{ 131 | Name: mapping.Tenant.GetName(), 132 | Namespace: mapping.Tenant.GetNamespace(), 133 | } 134 | 135 | t := obj.NewTenant(key) 136 | if _, err := t.Load(ctx, m.cl.APIClient); err != nil { 137 | return nil, err 138 | } 139 | 140 | t.Object.Spec = mapping.Tenant.Spec 141 | 142 | if err := t.Persist(ctx, m.cl.APIClient); err != nil { 143 | return nil, err 144 | } 145 | 146 | return mapping.Tenant, nil 147 | } 148 | 149 | func (m *Manager) CreateWorkflow(ctx context.Context, wd *v1.WorkflowData, t *relayv1beta1.Tenant) (*relayv1beta1.Workflow, error) { 150 | vm := newVaultManager(m.cl, m.cfg) 151 | am := newAdminManager(m.cl, vm) 152 | 153 | name := wd.Name 154 | if name == "" { 155 | name = defaultWorkflowName 156 | } 157 | 158 | // FIXME Refactor the connection handling (ideally not directly linked to the create workflow functionality) 159 | if err := am.reconcile(ctx); err != nil { 160 | return nil, err 161 | } 162 | if err := am.addConnectionForWorkflow(ctx, name); err != nil { 163 | return nil, err 164 | } 165 | 166 | mapper := v1.NewDefaultWorkflowMapper( 167 | v1.WithDomainIDOption(name), 168 | v1.WithNamespaceOption(tenantNamespace), 169 | v1.WithWorkflowNameOption(name), 170 | v1.WithVaultEngineMountOption(VaultEngineMountCustomers), 171 | v1.WithTenantOption(t), 172 | ) 173 | 174 | mapping, err := mapper.Map(wd) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | key := client.ObjectKey{ 180 | Name: mapping.Workflow.GetName(), 181 | Namespace: mapping.Workflow.GetNamespace(), 182 | } 183 | 184 | wf := obj.NewWorkflow(key) 185 | if _, err := wf.Load(ctx, m.cl.APIClient); err != nil { 186 | return nil, err 187 | } 188 | 189 | wf.Object.Spec = mapping.Workflow.Spec 190 | 191 | if err := wf.Persist(ctx, m.cl.APIClient); err != nil { 192 | return nil, err 193 | } 194 | 195 | return mapping.Workflow, nil 196 | } 197 | 198 | func (m *Manager) RunWorkflow(ctx context.Context, wf *relayv1beta1.Workflow, params map[string]string) (*relayv1beta1.Run, error) { 199 | runName := names.SimpleNameGenerator.GenerateName(wf.GetName() + "-") 200 | 201 | runParams := v1.WorkflowRunParameters{} 202 | 203 | for k, v := range params { 204 | runParams[k] = &v1.WorkflowRunParameter{ 205 | Value: v, 206 | } 207 | } 208 | 209 | mapper := v1.NewDefaultRunEngineMapper( 210 | v1.WithDomainIDRunOption(wf.GetName()), 211 | v1.WithNamespaceRunOption(wf.GetNamespace()), 212 | v1.WithWorkflowNameRunOption(wf.GetName()), 213 | v1.WithWorkflowRunNameRunOption(runName), 214 | v1.WithVaultEngineMountRunOption(VaultEngineMountCustomers), 215 | v1.WithRunParametersRunOption(runParams), 216 | v1.WithWorkflowRunOption(wf), 217 | ) 218 | 219 | mapping, err := mapper.ToRuntimeObjectsManifest() 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | if err := m.cl.APIClient.Create(ctx, mapping.Namespace); err != nil { 225 | if !apierrors.IsAlreadyExists(err) { 226 | return nil, err 227 | } 228 | } 229 | 230 | if err := m.cl.APIClient.Create(ctx, mapping.WorkflowRun); err != nil { 231 | return nil, err 232 | } 233 | 234 | return mapping.WorkflowRun, err 235 | } 236 | 237 | func (m *Manager) SetWorkflowSecret(ctx context.Context, workflow, key, value string) error { 238 | vm := newVaultManager(m.cl, m.cfg) 239 | secret := map[string]string{ 240 | path.Join(VaultEngineMountCustomers, VaultEngineMountWorkflows, workflow, key): value, 241 | } 242 | 243 | return vm.writeSecrets(ctx, secret) 244 | } 245 | 246 | func (m *Manager) InitializeRelayCore(ctx context.Context, initOpts InitializeOptions, installerOpts InstallerOptions, logServiceOpts LogServiceOptions) error { 247 | // I introduced some race condition where the cluster hasn't fully setup 248 | // the object APIs or something, so when we try to create objects here, it 249 | // will blow up saying the API for that object type doesn't exist. If we 250 | // sleep for just a second, then we give it enough time to fully warm up or 251 | // something. I dunno... 252 | // 253 | // There's an option in k3d's cluster create that I set to wait for the 254 | // server, but I think there's something deeper happening inside kubernetes 255 | // (probably in the API server). 256 | <-time.After(time.Second * 5) 257 | 258 | nm := newNamespaceManager(m.cl) 259 | rim := newRelayInstallerManager(m.cl, installerOpts) 260 | rcm := newRelayCoreManager(m.cl, installerOpts, logServiceOpts) 261 | 262 | if err := nm.reconcile(ctx); err != nil { 263 | return err 264 | } 265 | 266 | // TODO: dynamically generate the list as we process the manifests 267 | 268 | mm := NewManifestManager(m.cl) 269 | 270 | manifests := []string{ 271 | "/tekton", 272 | "/knative", 273 | "/relay", 274 | "/kourier", 275 | } 276 | 277 | if initOpts.InstallHelmController { 278 | manifests = append(manifests, "helm-controller") 279 | } 280 | 281 | for _, manifest := range manifests { 282 | if err := mm.ProcessManifests(ctx, manifest); err != nil { 283 | return err 284 | } 285 | } 286 | 287 | if err := rim.reconcile(ctx); err != nil { 288 | return err 289 | } 290 | 291 | if err := rcm.reconcile(ctx); err != nil { 292 | return err 293 | } 294 | 295 | return nil 296 | } 297 | 298 | func NewManager(ctx context.Context) (*Manager, error) { 299 | kcfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 300 | clientcmd.NewDefaultClientConfigLoadingRules(), 301 | &clientcmd.ConfigOverrides{}, 302 | ) 303 | 304 | apiConfig, err := kcfg.RawConfig() 305 | if err != nil { 306 | return nil, err 307 | } 308 | 309 | cl, err := NewClient(ctx, ClientOptions{Scheme: DefaultScheme}, &apiConfig) 310 | if err != nil { 311 | return nil, err 312 | } 313 | 314 | return &Manager{ 315 | cl: cl, 316 | cfg: Config{}, 317 | }, nil 318 | } 319 | 320 | func NewClient(ctx context.Context, opts ClientOptions, apiConfig *clientcmdapi.Config) (*Client, error) { 321 | overrides := &clientcmd.ConfigOverrides{} 322 | clientConfig := clientcmd.NewDefaultClientConfig(*apiConfig, overrides) 323 | 324 | restConfig, err := clientConfig.ClientConfig() 325 | if err != nil { 326 | return nil, err 327 | } 328 | 329 | c, err := client.New(restConfig, client.Options{ 330 | Scheme: opts.Scheme, 331 | }) 332 | if err != nil { 333 | return nil, err 334 | } 335 | 336 | mapper, err := apiutil.NewDynamicRESTMapper(restConfig) 337 | if err != nil { 338 | return nil, err 339 | } 340 | 341 | return &Client{ 342 | APIClient: c, 343 | Mapper: mapper, 344 | }, nil 345 | } 346 | -------------------------------------------------------------------------------- /pkg/dev/manifest.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/puppetlabs/leg/k8sutil/pkg/manifest" 7 | "github.com/puppetlabs/relay/pkg/dev/manifests" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | type ManifestManager struct { 12 | cl *Client 13 | } 14 | 15 | func (m *ManifestManager) ProcessManifests(ctx context.Context, path string, patchers ...manifest.PatcherFunc) error { 16 | files := manifests.MustAssetListDir(path) 17 | 18 | for _, file := range files { 19 | r := manifests.MustAsset(file) 20 | 21 | objs, err := manifest.Parse(DefaultScheme, r, patchers...) 22 | if err != nil { 23 | return nil 24 | } 25 | 26 | for _, obj := range objs { 27 | if err := m.cl.APIClient.Patch(ctx, obj, client.Apply, client.ForceOwnership, client.FieldOwner("relay")); err != nil { 28 | return err 29 | } 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func NewManifestManager(cl *Client) *ManifestManager { 37 | return &ManifestManager{ 38 | cl: cl, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/dev/manifests/asset.go: -------------------------------------------------------------------------------- 1 | package manifests 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "path/filepath" 7 | ) 8 | 9 | func Asset(name string) (io.ReadCloser, error) { 10 | return assets.Open(name) 11 | } 12 | 13 | func AssetString(name string) (string, error) { 14 | asset, err := Asset(name) 15 | if err != nil { 16 | return "", err 17 | } 18 | defer asset.Close() 19 | 20 | b, err := ioutil.ReadAll(asset) 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | return string(b), nil 26 | } 27 | 28 | func AssetListDir(path string) ([]string, error) { 29 | dir, err := assets.Open(path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | files, err := dir.Readdir(-1) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | names := []string{} 40 | 41 | for _, fi := range files { 42 | names = append(names, filepath.Join(path, fi.Name())) 43 | } 44 | 45 | return names, nil 46 | } 47 | 48 | func MustAsset(name string) io.ReadCloser { 49 | r, err := Asset(name) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | return r 55 | } 56 | 57 | func MustAssetString(name string) string { 58 | data, err := AssetString(name) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | return data 64 | } 65 | 66 | func MustAssetListDir(path string) []string { 67 | files, err := AssetListDir(path) 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | return files 73 | } 74 | -------------------------------------------------------------------------------- /pkg/dev/manifests/data/helm-controller/deploy-namespaced.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: helm-controller 5 | labels: 6 | name: helm-controller 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | name: helm-controller 12 | rules: 13 | - apiGroups: 14 | - "*" 15 | resources: 16 | - "*" 17 | verbs: 18 | - "*" 19 | --- 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | kind: ClusterRoleBinding 22 | metadata: 23 | name: helm-controller 24 | subjects: 25 | - kind: ServiceAccount 26 | name: default 27 | namespace: helm-controller 28 | roleRef: 29 | kind: ClusterRole 30 | name: helm-controller 31 | apiGroup: rbac.authorization.k8s.io 32 | --- 33 | apiVersion: apiextensions.k8s.io/v1beta1 34 | kind: CustomResourceDefinition 35 | metadata: 36 | name: helmcharts.helm.cattle.io 37 | namespace: helm-controller 38 | spec: 39 | group: helm.cattle.io 40 | version: v1 41 | names: 42 | kind: HelmChart 43 | plural: helmcharts 44 | singular: helmchart 45 | scope: Namespaced 46 | --- 47 | apiVersion: apps/v1 48 | kind: Deployment 49 | metadata: 50 | name: helm-controller 51 | namespace: helm-controller 52 | labels: 53 | app: helm-controller 54 | spec: 55 | replicas: 1 56 | selector: 57 | matchLabels: 58 | app: helm-controller 59 | template: 60 | metadata: 61 | labels: 62 | app: helm-controller 63 | spec: 64 | containers: 65 | - name: helm-controller 66 | image: rancher/helm-controller:v0.2.1 67 | command: ["helm-controller"] 68 | -------------------------------------------------------------------------------- /pkg/dev/manifests/data/knative/3-patch-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-domain 5 | namespace: knative-serving 6 | data: 7 | svc.cluster.local: '' 8 | --- 9 | apiVersion: v1 10 | kind: ConfigMap 11 | metadata: 12 | name: config-network 13 | namespace: knative-serving 14 | data: 15 | ingress.class: kourier.ingress.networking.knative.dev 16 | -------------------------------------------------------------------------------- /pkg/dev/manifests/data/relay/relay.sh_webhooktriggers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: webhooktriggers.relay.sh 9 | spec: 10 | group: relay.sh 11 | names: 12 | kind: WebhookTrigger 13 | listKind: WebhookTriggerList 14 | plural: webhooktriggers 15 | singular: webhooktrigger 16 | scope: Namespaced 17 | versions: 18 | - name: v1beta1 19 | schema: 20 | openAPIV3Schema: 21 | description: WebhookTrigger represents a definition of a webhook to receive 22 | events. 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | properties: 38 | args: 39 | description: Args are the command arguments. 40 | items: 41 | type: string 42 | type: array 43 | command: 44 | description: Command is the path to the executable to run when the 45 | container starts. 46 | type: string 47 | env: 48 | additionalProperties: 49 | description: Unstructured is arbitrary JSON data, which may also 50 | include base64-encoded binary data. 51 | x-kubernetes-preserve-unknown-fields: true 52 | description: Env allows environment variables to be provided to the 53 | container image. 54 | type: object 55 | image: 56 | description: Image is the Docker image to run when this webhook receives 57 | an event. 58 | type: string 59 | input: 60 | description: Input is the input script to provide to the container. 61 | items: 62 | type: string 63 | type: array 64 | name: 65 | description: Name is a friendly name for this webhook trigger used 66 | for authentication and reporting. 67 | type: string 68 | spec: 69 | additionalProperties: 70 | description: Unstructured is arbitrary JSON data, which may also 71 | include base64-encoded binary data. 72 | x-kubernetes-preserve-unknown-fields: true 73 | description: Spec is the Relay specification to be provided to the 74 | container image. 75 | type: object 76 | tenantRef: 77 | description: TenantRef selects the tenant to apply this trigger to. 78 | properties: 79 | name: 80 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 81 | TODO: Add other useful fields. apiVersion, kind, uid?' 82 | type: string 83 | type: object 84 | required: 85 | - image 86 | - tenantRef 87 | type: object 88 | status: 89 | properties: 90 | conditions: 91 | description: Conditions are the observations of this resource's tate. 92 | items: 93 | properties: 94 | lastTransitionTime: 95 | format: date-time 96 | type: string 97 | message: 98 | description: Message is a human-readable description of the 99 | given status. 100 | type: string 101 | reason: 102 | description: Reason identifies the cause of the given status 103 | using an API-locked camel-case identifier. 104 | type: string 105 | status: 106 | type: string 107 | type: 108 | description: Type is the identifier for this condition. 109 | enum: 110 | - ServiceReady 111 | - Ready 112 | type: string 113 | required: 114 | - lastTransitionTime 115 | - status 116 | - type 117 | type: object 118 | type: array 119 | x-kubernetes-list-map-keys: 120 | - type 121 | x-kubernetes-list-type: map 122 | namespace: 123 | description: Namespace is the Kubernetes namespace containing the 124 | target resources of this webhook trigger. 125 | type: string 126 | observedGeneration: 127 | description: ObservedGeneration is the generation of the resource 128 | specification that this status matches. 129 | format: int64 130 | type: integer 131 | url: 132 | description: URL is the endpoint for the webhook once provisioned. 133 | type: string 134 | type: object 135 | required: 136 | - spec 137 | type: object 138 | served: true 139 | storage: true 140 | subresources: 141 | status: {} 142 | status: 143 | acceptedNames: 144 | kind: "" 145 | plural: "" 146 | conditions: [] 147 | storedVersions: [] 148 | -------------------------------------------------------------------------------- /pkg/dev/manifests/data/relay/relay.sh_workflows.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: workflows.relay.sh 9 | spec: 10 | group: relay.sh 11 | names: 12 | kind: Workflow 13 | listKind: WorkflowList 14 | plural: workflows 15 | singular: workflow 16 | scope: Namespaced 17 | versions: 18 | - name: v1beta1 19 | schema: 20 | openAPIV3Schema: 21 | description: Workflow represents a set of steps that Relay can execute. 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | properties: 37 | parameters: 38 | description: Parameters are the definitions of parameters used by 39 | this workflow. 40 | items: 41 | properties: 42 | default: 43 | description: Value is the default value for this parameter. 44 | If not specified, a value must be provided at runtime. 45 | x-kubernetes-preserve-unknown-fields: true 46 | name: 47 | description: Name is a unique name for this parameter. 48 | type: string 49 | required: 50 | - name 51 | type: object 52 | type: array 53 | x-kubernetes-list-map-keys: 54 | - name 55 | x-kubernetes-list-type: map 56 | steps: 57 | description: Steps are the individual steps that make up the workflow. 58 | items: 59 | properties: 60 | args: 61 | description: Args are the command arguments. 62 | items: 63 | type: string 64 | type: array 65 | command: 66 | description: Command is the path to the executable to run when 67 | the container starts. 68 | type: string 69 | dependsOn: 70 | description: DependsOn causes this step to run after the given 71 | step names. 72 | items: 73 | type: string 74 | type: array 75 | env: 76 | additionalProperties: 77 | description: Unstructured is arbitrary JSON data, which may 78 | also include base64-encoded binary data. 79 | x-kubernetes-preserve-unknown-fields: true 80 | description: Env allows environment variables to be provided 81 | to the container image. 82 | type: object 83 | image: 84 | description: Image is the Docker image to run when this webhook 85 | receives an event. 86 | type: string 87 | input: 88 | description: Input is the input script to provide to the container. 89 | items: 90 | type: string 91 | type: array 92 | name: 93 | description: Name is a unique name for this step. 94 | type: string 95 | spec: 96 | additionalProperties: 97 | description: Unstructured is arbitrary JSON data, which may 98 | also include base64-encoded binary data. 99 | x-kubernetes-preserve-unknown-fields: true 100 | description: Spec is the Relay specification to be provided 101 | to the container image. 102 | type: object 103 | when: 104 | description: When provides a set of conditions that must be 105 | met for this step to run. 106 | x-kubernetes-preserve-unknown-fields: true 107 | required: 108 | - image 109 | - name 110 | type: object 111 | type: array 112 | x-kubernetes-list-map-keys: 113 | - name 114 | x-kubernetes-list-type: map 115 | tenantRef: 116 | description: TenantRef selects the tenant to use for this workflow. 117 | properties: 118 | name: 119 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 120 | TODO: Add other useful fields. apiVersion, kind, uid?' 121 | type: string 122 | type: object 123 | required: 124 | - tenantRef 125 | type: object 126 | required: 127 | - spec 128 | type: object 129 | served: true 130 | storage: true 131 | status: 132 | acceptedNames: 133 | kind: "" 134 | plural: "" 135 | conditions: [] 136 | storedVersions: [] 137 | -------------------------------------------------------------------------------- /pkg/dev/manifests/generate.go: -------------------------------------------------------------------------------- 1 | //go:generate go run generate_tool.go 2 | 3 | package manifests 4 | -------------------------------------------------------------------------------- /pkg/dev/manifests/generate_tool.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | 9 | "github.com/puppetlabs/leg/httputil/fs" 10 | "github.com/shurcooL/vfsgen" 11 | ) 12 | 13 | var h = fs.DirWithoutModTimes("data") 14 | 15 | func main() { 16 | err := vfsgen.Generate(h, vfsgen.Options{ 17 | Filename: "generate_assets.go", 18 | PackageName: os.Getenv("GOPACKAGE"), 19 | }) 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/dev/metadata.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/puppetlabs/errawr-go/v2/pkg/errawr" 12 | "github.com/puppetlabs/leg/httputil/serving" 13 | "github.com/puppetlabs/relay-core/pkg/metadataapi/opt" 14 | "github.com/puppetlabs/relay-core/pkg/metadataapi/sample" 15 | "github.com/puppetlabs/relay-core/pkg/metadataapi/server" 16 | "github.com/puppetlabs/relay-core/pkg/metadataapi/server/middleware" 17 | ) 18 | 19 | type MetadataAPIManager struct { 20 | cfg Config 21 | } 22 | 23 | type MetadataMockOptions struct { 24 | RunID string 25 | StepName string 26 | Input string 27 | } 28 | 29 | func (m *MetadataAPIManager) InitializeMetadataApi(ctx context.Context, mockOptions MetadataMockOptions) (string, error) { 30 | dynamicAddr := make(chan string) 31 | s, token, err := m.initializeMetadataServer(ctx, dynamicAddr, mockOptions) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | var listenOpts []serving.ListenWaitHTTPOption 37 | go func() { 38 | // This will end by the closer when the ctx is marked done 39 | err = serving.ListenWaitHTTP(ctx, s, listenOpts...) 40 | if err != nil { 41 | println(fmt.Errorf("couldn't start metadata service: %v", err.Error())) 42 | os.Exit(1) 43 | } 44 | }() 45 | 46 | return fmt.Sprintf("http://:%s@%s", token, <-dynamicAddr), nil 47 | } 48 | 49 | func (m *MetadataAPIManager) initializeMetadataServer(ctx context.Context, addr chan string, mockOptions MetadataMockOptions) (*http.Server, string, error) { 50 | var auth middleware.Authenticator 51 | var tm *sample.TokenMap 52 | cfg := opt.NewConfig() 53 | cfg.ListenPort = 0 54 | cfg.SampleConfigFiles = []string{mockOptions.Input} 55 | 56 | if sc, err := cfg.SampleConfig(); err != nil { 57 | return nil, "", err 58 | } else if sc != nil { 59 | var key []byte 60 | 61 | if ek := cfg.SampleHS256SigningKey; ek != "" { 62 | var err error 63 | 64 | key, err = base64.StdEncoding.DecodeString(ek) 65 | if err != nil { 66 | return nil, "", fmt.Errorf("could not decode signing key: %+v", err) 67 | } 68 | } 69 | 70 | tg, err := sample.NewHS256TokenGenerator(key) 71 | if err != nil { 72 | return nil, "", fmt.Errorf("failed to create token generator: %+v", err) 73 | } 74 | 75 | tm = tg.GenerateAll(ctx, sc) 76 | 77 | auth = sample.NewAuthenticator(sc, tg.Key()) 78 | } 79 | var serverOpts []server.Option 80 | serverOpts = append(serverOpts, server.WithErrorSensitivity(errawr.ErrorSensitivityAll)) 81 | 82 | s := &http.Server{ 83 | Handler: server.NewHandler(auth, serverOpts...), 84 | Addr: fmt.Sprintf("0.0.0.0:%d", cfg.ListenPort), 85 | BaseContext: func(l net.Listener) context.Context { 86 | addr <- l.Addr().String() 87 | return context.Background() 88 | }, 89 | } 90 | token, found := tm.ForStep(mockOptions.RunID, mockOptions.StepName) 91 | if !found { 92 | return nil, "", fmt.Errorf("failed to find run ID %s with a step named %s in %s", mockOptions.RunID, mockOptions.StepName, mockOptions.Input) 93 | } 94 | 95 | return s, token, nil 96 | } 97 | 98 | func NewMetadataAPIManager(cfg Config) *MetadataAPIManager { 99 | return &MetadataAPIManager{ 100 | cfg: cfg, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/dev/namespace.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | const ( 13 | systemNamespace = "relay-system" 14 | tenantNamespace = "relay-tenants" 15 | 16 | knativeServingNamespace = "knative-serving" 17 | kourierSystemNamespace = "kourier-system" 18 | tektonPipelinesNamespace = "tekton-pipelines" 19 | ) 20 | 21 | type namespaceObjects struct { 22 | systemNamespace corev1.Namespace 23 | knativeServingNamespace corev1.Namespace 24 | kourierSystemNamespace corev1.Namespace 25 | } 26 | 27 | func newNamespaceObjects() *namespaceObjects { 28 | return &namespaceObjects{ 29 | systemNamespace: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: systemNamespace}}, 30 | knativeServingNamespace: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: knativeServingNamespace}}, 31 | kourierSystemNamespace: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: kourierSystemNamespace}}, 32 | } 33 | } 34 | 35 | type namespaceManager struct { 36 | cl *Client 37 | objects *namespaceObjects 38 | } 39 | 40 | func (m *namespaceManager) reconcile(ctx context.Context) error { 41 | cl := m.cl.APIClient 42 | 43 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.systemNamespace, func() error { 44 | m.systemNamespace(&m.objects.systemNamespace) 45 | 46 | return nil 47 | }); err != nil { 48 | return err 49 | } 50 | 51 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.knativeServingNamespace, func() error { 52 | m.knativeServingNamespace(&m.objects.knativeServingNamespace) 53 | 54 | return nil 55 | }); err != nil { 56 | return err 57 | } 58 | 59 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.knativeServingNamespace, func() error { 60 | m.kourierSystemNamespace(&m.objects.kourierSystemNamespace) 61 | 62 | return nil 63 | }); err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (m *namespaceManager) systemNamespace(ns *corev1.Namespace) { 71 | ns.Labels = map[string]string{ 72 | "nebula.puppet.com/network-policy.tasks": "true", 73 | } 74 | } 75 | 76 | func (m *namespaceManager) knativeServingNamespace(ns *corev1.Namespace) { 77 | ns.Labels = map[string]string{ 78 | "nebula.puppet.com/network-policy.webhook-gateway": "true", 79 | } 80 | } 81 | 82 | func (m *namespaceManager) kourierSystemNamespace(ns *corev1.Namespace) { 83 | ns.Labels = map[string]string{ 84 | "nebula.puppet.com/network-policy.webhook-gateway": "true", 85 | } 86 | } 87 | 88 | func (m *namespaceManager) delete(ctx context.Context, ns string) error { 89 | namespace := &corev1.Namespace{ 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: ns, 92 | }, 93 | } 94 | 95 | return m.cl.APIClient.Delete(ctx, namespace, client.PropagationPolicy(metav1.DeletePropagationBackground)) 96 | } 97 | 98 | func newNamespaceManager(cl *Client) *namespaceManager { 99 | return &namespaceManager{ 100 | cl: cl, 101 | objects: newNamespaceObjects(), 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/dev/relaycore.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | installerv1alpha1 "github.com/puppetlabs/relay-core/pkg/apis/install.relay.sh/v1alpha1" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/api/resource" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | const ( 15 | RelayInstallerImage = "us-docker.pkg.dev/puppet-relay-contrib-oss/relay-core/relay-installer" 16 | RelayMetadataAPIImage = "us-docker.pkg.dev/puppet-relay-contrib-oss/relay-core/relay-metadata-api" 17 | RelayOperatorImage = "us-docker.pkg.dev/puppet-relay-contrib-oss/relay-core/relay-operator" 18 | RelayOperatorVaultInitImage = "us-docker.pkg.dev/puppet-relay-contrib-oss/relay-core/relay-operator-vault-init" 19 | RelayOperatorWebhookCertificateControllerImage = "us-docker.pkg.dev/puppet-relay-contrib-oss/relay-core/relay-operator-webhook-certificate-controller" 20 | 21 | RelayLogServiceImage = "relaysh/relay-pls:latest" 22 | ) 23 | 24 | const ( 25 | DefaultVaultConfiguration = ` 26 | disable_mlock = true 27 | ui = true 28 | log_level = "Debug" 29 | listener "tcp" { 30 | tls_disable = 1 31 | address = "0.0.0.0:8200" 32 | } 33 | plugin_directory = "/relay/vault/plugins" 34 | storage "file" { 35 | path = "/vault/data" 36 | }` 37 | DefaultVaultConfigurationFile = "vault.hcl" 38 | DefaultVaultServerImage = "relaysh/relay-vault:latest" 39 | DefaultVaultSidecarImage = "vault:latest" 40 | ) 41 | 42 | const ( 43 | relayCoreName = "relay-core-v1" 44 | ) 45 | 46 | type relayCoreObjects struct { 47 | configMap corev1.ConfigMap 48 | relayCore installerv1alpha1.RelayCore 49 | serviceAccount corev1.ServiceAccount 50 | } 51 | 52 | func newRelayCoreObjects() *relayCoreObjects { 53 | objectMeta := metav1.ObjectMeta{ 54 | Name: relayCoreName, 55 | Namespace: systemNamespace, 56 | } 57 | 58 | operatorObjectMeta := objectMeta 59 | operatorObjectMeta.Name = fmt.Sprintf("%s-operator", objectMeta.Name) 60 | 61 | return &relayCoreObjects{ 62 | configMap: corev1.ConfigMap{ObjectMeta: operatorObjectMeta}, 63 | relayCore: installerv1alpha1.RelayCore{ObjectMeta: objectMeta}, 64 | serviceAccount: corev1.ServiceAccount{ObjectMeta: operatorObjectMeta}, 65 | } 66 | } 67 | 68 | type relayCoreManager struct { 69 | cl *Client 70 | objects *relayCoreObjects 71 | installerOpts InstallerOptions 72 | logServiceOpts LogServiceOptions 73 | } 74 | 75 | func (m *relayCoreManager) reconcile(ctx context.Context) error { 76 | cl := m.cl.APIClient 77 | 78 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.configMap, func() error { 79 | m.operatorConfigMap(&m.objects.configMap) 80 | 81 | return nil 82 | }); err != nil { 83 | return err 84 | } 85 | 86 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.relayCore, func() error { 87 | m.relayCore(&m.objects.relayCore) 88 | 89 | return nil 90 | }); err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (m *relayCoreManager) operatorConfigMap(configMap *corev1.ConfigMap) { 98 | configMap.Data = map[string]string{ 99 | DefaultVaultConfigurationFile: DefaultVaultConfiguration, 100 | } 101 | } 102 | 103 | func (m *relayCoreManager) relayCore(rc *installerv1alpha1.RelayCore) { 104 | rc.Spec.LogService = &installerv1alpha1.LogServiceConfig{ 105 | Image: m.installerOpts.LogServiceImage, 106 | ImagePullPolicy: corev1.PullAlways, 107 | } 108 | 109 | if m.logServiceOpts.CredentialsSecretName != "" && m.logServiceOpts.CredentialsKey != "" { 110 | rc.Spec.LogService.CredentialsSecretKeyRef = &corev1.SecretKeySelector{ 111 | Key: m.logServiceOpts.CredentialsKey, 112 | LocalObjectReference: corev1.LocalObjectReference{ 113 | Name: m.logServiceOpts.CredentialsSecretName, 114 | }, 115 | } 116 | rc.Spec.LogService.Project = m.logServiceOpts.Project 117 | rc.Spec.LogService.Dataset = m.logServiceOpts.Dataset 118 | rc.Spec.LogService.Table = m.logServiceOpts.Table 119 | } 120 | 121 | tn := tenantNamespace 122 | rc.Spec.Operator = installerv1alpha1.OperatorConfig{ 123 | Image: m.installerOpts.OperatorImage, 124 | ImagePullPolicy: corev1.PullAlways, 125 | TenantNamespace: &tn, 126 | Standalone: true, 127 | AdmissionWebhookServer: &installerv1alpha1.AdmissionWebhookServerConfig{ 128 | CertificateControllerImage: m.installerOpts.OperatorWebhookCertificateControllerImage, 129 | CertificateControllerImagePullPolicy: corev1.PullAlways, 130 | Domain: "admission.controller.relay.sh", 131 | NamespaceSelector: &metav1.LabelSelector{ 132 | MatchLabels: map[string]string{ 133 | "controller.relay.sh/tenant-workload": "true", 134 | }, 135 | }, 136 | }, 137 | } 138 | 139 | rc.Spec.MetadataAPI = installerv1alpha1.MetadataAPIConfig{ 140 | Image: m.installerOpts.MetadataAPIImage, 141 | ImagePullPolicy: corev1.PullAlways, 142 | } 143 | 144 | rc.Spec.Vault = installerv1alpha1.VaultConfig{ 145 | Engine: installerv1alpha1.VaultEngineConfig{ 146 | VaultInitializationImage: m.installerOpts.OperatorVaultInitImage, 147 | VaultInitializationImagePullPolicy: corev1.PullAlways, 148 | 149 | // FIXME Change this to be more flexible/specific 150 | AuthDelegatorServiceAccountName: vaultIdentifier, 151 | }, 152 | Server: installerv1alpha1.VaultServerConfig{ 153 | BuiltIn: &installerv1alpha1.VaultServerBuiltInConfig{ 154 | Image: m.installerOpts.VaultServerImage, 155 | ImagePullPolicy: corev1.PullAlways, 156 | Resources: corev1.ResourceRequirements{ 157 | Limits: map[corev1.ResourceName]resource.Quantity{ 158 | corev1.ResourceCPU: resource.MustParse("50m"), 159 | corev1.ResourceMemory: resource.MustParse("64Mi"), 160 | }, 161 | Requests: map[corev1.ResourceName]resource.Quantity{ 162 | corev1.ResourceCPU: resource.MustParse("25m"), 163 | corev1.ResourceMemory: resource.MustParse("64Mi"), 164 | }, 165 | }, 166 | ConfigMapRef: corev1.LocalObjectReference{ 167 | Name: m.objects.configMap.Name, 168 | }, 169 | }, 170 | }, 171 | Sidecar: installerv1alpha1.VaultSidecarConfig{ 172 | Image: m.installerOpts.VaultSidecarImage, 173 | ImagePullPolicy: corev1.PullAlways, 174 | Resources: corev1.ResourceRequirements{ 175 | Limits: map[corev1.ResourceName]resource.Quantity{ 176 | corev1.ResourceCPU: resource.MustParse("50m"), 177 | corev1.ResourceMemory: resource.MustParse("64Mi"), 178 | }, 179 | Requests: map[corev1.ResourceName]resource.Quantity{ 180 | corev1.ResourceCPU: resource.MustParse("25m"), 181 | corev1.ResourceMemory: resource.MustParse("64Mi"), 182 | }, 183 | }, 184 | }, 185 | } 186 | } 187 | 188 | func newRelayCoreManager(cl *Client, installerOpts InstallerOptions, logServiceOpts LogServiceOptions) *relayCoreManager { 189 | return &relayCoreManager{ 190 | cl: cl, 191 | objects: newRelayCoreObjects(), 192 | installerOpts: installerOpts, 193 | logServiceOpts: logServiceOpts, 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /pkg/dev/relayinstaller.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | rbacv1 "k8s.io/api/rbac/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ctrl "sigs.k8s.io/controller-runtime" 11 | ) 12 | 13 | type relayInstallerObjects struct { 14 | serviceAccount corev1.ServiceAccount 15 | clusterRole rbacv1.ClusterRole 16 | clusterRoleBinding rbacv1.ClusterRoleBinding 17 | deployment appsv1.Deployment 18 | } 19 | 20 | func newRelayInstallerObjects() *relayInstallerObjects { 21 | objectMeta := metav1.ObjectMeta{ 22 | Name: "relay-installer", 23 | Namespace: "relay-installer", 24 | } 25 | 26 | return &relayInstallerObjects{ 27 | serviceAccount: corev1.ServiceAccount{ObjectMeta: objectMeta}, 28 | clusterRole: rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: objectMeta.Name}}, 29 | clusterRoleBinding: rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: objectMeta.Name}}, 30 | deployment: appsv1.Deployment{ObjectMeta: objectMeta}, 31 | } 32 | } 33 | 34 | type relayInstallerManager struct { 35 | cl *Client 36 | objects *relayInstallerObjects 37 | installerOpts InstallerOptions 38 | } 39 | 40 | func (m *relayInstallerManager) reconcile(ctx context.Context) error { 41 | cl := m.cl.APIClient 42 | 43 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.serviceAccount, func() error { 44 | m.serviceAccount(&m.objects.serviceAccount) 45 | 46 | return nil 47 | }); err != nil { 48 | return err 49 | } 50 | 51 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.clusterRole, func() error { 52 | m.clusterRole(&m.objects.clusterRole) 53 | 54 | return nil 55 | }); err != nil { 56 | return err 57 | } 58 | 59 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.clusterRoleBinding, func() error { 60 | m.clusterRoleBinding(&m.objects.clusterRoleBinding) 61 | 62 | return nil 63 | }); err != nil { 64 | return err 65 | } 66 | 67 | if _, err := ctrl.CreateOrUpdate(ctx, cl, &m.objects.deployment, func() error { 68 | m.deployment(&m.objects.deployment) 69 | 70 | return nil 71 | }); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (m *relayInstallerManager) serviceAccount(sa *corev1.ServiceAccount) { 79 | sa.Labels = m.labels() 80 | } 81 | 82 | // clusterRole configures the roles requires for the installer to run. It has 83 | // to delegate roles and bindings to relay-operator and relay-metadata-api via 84 | // the creation of clusterroles/roles and clusterrolebindings/rolebindings, so 85 | // in order to do that, it itself needs bindings to a large amount of resources 86 | // and verbs. It would be nice if this was autogenerated somehow, because as we 87 | // change relay-core controllers, we are going to need to reflect those rbac 88 | // policies here as well. 89 | func (m *relayInstallerManager) clusterRole(cr *rbacv1.ClusterRole) { 90 | cr.Labels = m.labels() 91 | 92 | verbAllGroups := []string{ 93 | "", 94 | "networking.k8s.io", 95 | "rbac.authorization.k8s.io", 96 | "install.relay.sh", 97 | "tekton.dev", 98 | "serving.knative.dev", 99 | } 100 | 101 | verbAllResources := []string{ 102 | "configmaps", 103 | "limitranges", 104 | "namespaces", 105 | "secrets", 106 | "serviceaccounts", 107 | "revisions", 108 | "services", 109 | "networkpolicies", 110 | "roles", 111 | "rolebindings", 112 | "relaycores", 113 | "conditions", 114 | "pipelineruns", 115 | "pipelines", 116 | "taskruns", 117 | "tasks", 118 | } 119 | 120 | cr.Rules = []rbacv1.PolicyRule{ 121 | {APIGroups: verbAllGroups, Resources: verbAllResources, Verbs: []string{rbacv1.VerbAll}}, 122 | { 123 | APIGroups: []string{"authentication.k8s.io"}, 124 | Resources: []string{"tokenreviews"}, 125 | Verbs: []string{"create"}, 126 | }, 127 | { 128 | APIGroups: []string{"authorization.k8s.io"}, 129 | Resources: []string{"subjectaccessreviews"}, 130 | Verbs: []string{"create"}, 131 | }, 132 | { 133 | APIGroups: []string{"apps", "rbac.authorization.k8s.io", "admissionregistration.k8s.io"}, 134 | Resources: []string{"clusterroles", "clusterrolebindings", "deployments", "mutatingwebhookconfigurations", "statefulsets"}, 135 | Verbs: []string{"create", "get", "list", "patch", "update", "watch"}, 136 | }, 137 | { 138 | APIGroups: []string{""}, 139 | Resources: []string{"pods", "pods/log"}, 140 | Verbs: []string{"get", "list", "watch"}, 141 | }, 142 | { 143 | APIGroups: []string{"batch"}, 144 | Resources: []string{"jobs"}, 145 | Verbs: []string{"create", "get", "list", "patch", "update", "watch"}, 146 | }, 147 | { 148 | APIGroups: []string{"install.relay.sh"}, 149 | Resources: []string{"relaycores/status"}, 150 | Verbs: []string{"get", "patch", "update"}, 151 | }, 152 | { 153 | APIGroups: []string{"pvpool.puppet.com"}, 154 | Resources: []string{"checkouts", "checkouts/status"}, 155 | Verbs: []string{"get", "list", "watch"}, 156 | }, 157 | { 158 | APIGroups: []string{"relay.sh"}, 159 | Resources: []string{"runs", "runs/status", "tenants", "tenants/status", "webhooktriggers", "webhooktriggers/status", "workflows", "workflows/status"}, 160 | Verbs: []string{"get", "list", "patch", "update", "watch"}, 161 | }, 162 | } 163 | } 164 | 165 | func (m *relayInstallerManager) clusterRoleBinding(crb *rbacv1.ClusterRoleBinding) { 166 | crb.Labels = m.labels() 167 | 168 | crb.RoleRef = rbacv1.RoleRef{ 169 | APIGroup: "rbac.authorization.k8s.io", 170 | Kind: "ClusterRole", 171 | Name: m.objects.clusterRole.Name, 172 | } 173 | 174 | crb.Subjects = []rbacv1.Subject{ 175 | { 176 | Kind: "ServiceAccount", 177 | Name: m.objects.serviceAccount.Name, 178 | Namespace: m.objects.serviceAccount.Namespace, 179 | }, 180 | } 181 | } 182 | 183 | func (m *relayInstallerManager) deployment(deployment *appsv1.Deployment) { 184 | deployment.Labels = m.labels() 185 | deployment.Spec.Selector = &metav1.LabelSelector{ 186 | MatchLabels: m.labels(), 187 | } 188 | 189 | template := &deployment.Spec.Template 190 | template.Labels = m.labels() 191 | 192 | template.Spec.RestartPolicy = corev1.RestartPolicyAlways 193 | template.Spec.ServiceAccountName = m.objects.serviceAccount.Name 194 | 195 | container := corev1.Container{ 196 | Name: "controller", 197 | Image: m.installerOpts.InstallerImage, 198 | ImagePullPolicy: corev1.PullAlways, 199 | } 200 | 201 | template.Spec.Containers = []corev1.Container{container} 202 | } 203 | 204 | func (m *relayInstallerManager) labels() map[string]string { 205 | return map[string]string{ 206 | "app.kubernetes.io/name": "relay-installer", 207 | "app.kubernetes.io/component": "controller", 208 | } 209 | } 210 | 211 | func newRelayInstallerManager(cl *Client, installerOpts InstallerOptions) *relayInstallerManager { 212 | return &relayInstallerManager{ 213 | cl: cl, 214 | objects: newRelayInstallerObjects(), 215 | installerOpts: installerOpts, 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /pkg/dev/vault.go: -------------------------------------------------------------------------------- 1 | package dev 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/puppetlabs/leg/timeutil/pkg/backoff" 11 | "github.com/puppetlabs/leg/timeutil/pkg/retry" 12 | batchv1 "k8s.io/api/batch/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | ) 17 | 18 | const ( 19 | vaultIdentifier = "vault" 20 | vaultImage = "vault:1.8.2" 21 | vaultAddr = "http://vault:8200" 22 | ) 23 | 24 | var vaultWriteValuesJobTTL = int32(120) 25 | 26 | type vaultManagerObjects struct { 27 | credentialsSecret corev1.Secret 28 | serviceAccount corev1.ServiceAccount 29 | } 30 | 31 | func newVaultManagerObjects() *vaultManagerObjects { 32 | objectMeta := metav1.ObjectMeta{ 33 | Name: vaultIdentifier, 34 | Namespace: systemNamespace, 35 | } 36 | 37 | return &vaultManagerObjects{ 38 | credentialsSecret: corev1.Secret{ObjectMeta: objectMeta}, 39 | serviceAccount: corev1.ServiceAccount{ObjectMeta: objectMeta}, 40 | } 41 | } 42 | 43 | type vaultManager struct { 44 | cl *Client 45 | objects *vaultManagerObjects 46 | 47 | cfg Config 48 | } 49 | 50 | func (m *vaultManager) baseJob(job *batchv1.Job) { 51 | job.Spec = batchv1.JobSpec{ 52 | Template: corev1.PodTemplateSpec{ 53 | Spec: corev1.PodSpec{ 54 | RestartPolicy: corev1.RestartPolicyNever, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func (m *vaultManager) baseJobContainer(container *corev1.Container) { 61 | container.Name = "vault-action" 62 | container.Image = vaultImage 63 | container.Env = []corev1.EnvVar{ 64 | {Name: "VAULT_ADDR", Value: vaultAddr}, 65 | } 66 | } 67 | 68 | func (m *vaultManager) credentialsEnvs(container *corev1.Container) { 69 | envs := []corev1.EnvVar{ 70 | { 71 | Name: "VAULT_TOKEN", 72 | ValueFrom: &corev1.EnvVarSource{ 73 | SecretKeyRef: &corev1.SecretKeySelector{ 74 | Key: "root-token", 75 | LocalObjectReference: corev1.LocalObjectReference{ 76 | Name: m.objects.credentialsSecret.Name, 77 | }, 78 | }, 79 | }, 80 | }, 81 | { 82 | Name: "VAULT_UNSEAL_KEY", 83 | ValueFrom: &corev1.EnvVarSource{ 84 | SecretKeyRef: &corev1.SecretKeySelector{ 85 | Key: "unseal-key", 86 | LocalObjectReference: corev1.LocalObjectReference{ 87 | Name: m.objects.credentialsSecret.Name, 88 | }, 89 | }, 90 | }, 91 | }, 92 | } 93 | 94 | container.Env = append(container.Env, envs...) 95 | } 96 | 97 | func (m *vaultManager) writeValuesJob(vals map[string]string, job *batchv1.Job) { 98 | m.baseJob(job) 99 | 100 | container := corev1.Container{} 101 | 102 | m.baseJobContainer(&container) 103 | m.credentialsEnvs(&container) 104 | 105 | cmds := []string{} 106 | 107 | for k, v := range vals { 108 | cmds = append(cmds, fmt.Sprintf("vault kv put %s value='%s'", k, v)) 109 | } 110 | 111 | script := strings.Join(cmds, "; ") 112 | 113 | container.Command = []string{"/bin/sh", "-c", script} 114 | 115 | job.Spec.Template.Spec.Containers = []corev1.Container{container} 116 | job.Spec.TTLSecondsAfterFinished = &vaultWriteValuesJobTTL 117 | } 118 | 119 | func (m *vaultManager) writeSecrets(ctx context.Context, vals map[string]string) error { 120 | job := batchv1.Job{ObjectMeta: metav1.ObjectMeta{ 121 | GenerateName: "vault-write-values-", 122 | Namespace: systemNamespace, 123 | }} 124 | 125 | m.writeValuesJob(vals, &job) 126 | 127 | if err := m.cl.APIClient.Create(ctx, &job); err != nil { 128 | return err 129 | } 130 | 131 | if err := m.waitForJobCompletion(ctx, &job); err != nil { 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (m *vaultManager) getJob(ctx context.Context, job *batchv1.Job) error { 139 | cl := m.cl.APIClient 140 | 141 | key := client.ObjectKeyFromObject(job) 142 | 143 | return cl.Get(ctx, key, job) 144 | } 145 | 146 | func (m *vaultManager) waitForJobCompletion(ctx context.Context, job *batchv1.Job) error { 147 | err := retry.Wait(ctx, func(ctx context.Context) (bool, error) { 148 | if err := m.getJob(ctx, job); err != nil { 149 | return retry.Repeat(err) 150 | } 151 | 152 | if len(job.Status.Conditions) == 0 { 153 | return retry.Repeat(errors.New("waiting for vault operator job to finish")) 154 | } 155 | 156 | for _, cond := range job.Status.Conditions { 157 | switch cond.Type { 158 | case batchv1.JobComplete: 159 | if cond.Status == corev1.ConditionTrue { 160 | return retry.Done(nil) 161 | } 162 | case batchv1.JobFailed: 163 | if cond.Status == corev1.ConditionTrue { 164 | return retry.Done(errors.New(cond.Message)) 165 | } 166 | } 167 | } 168 | 169 | return retry.Repeat(nil) 170 | }, 171 | retry.WithBackoffFactory( 172 | backoff.Build( 173 | backoff.Exponential(100*time.Millisecond, 2.0), 174 | backoff.MaxBound(1*time.Minute), 175 | backoff.MaxRetries(20), 176 | ), 177 | ), 178 | ) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | return nil 184 | } 185 | 186 | func (m *vaultManager) cleanupJobs(ctx context.Context, jobs []*batchv1.Job) error { 187 | for _, job := range jobs { 188 | policy := client.PropagationPolicy(metav1.DeletePropagationBackground) 189 | 190 | if err := m.cl.APIClient.Delete(ctx, job, policy); err != nil { 191 | return err 192 | } 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func newVaultManager(cl *Client, cfg Config) *vaultManager { 199 | return &vaultManager{ 200 | cl: cl, 201 | objects: newVaultManagerObjects(), 202 | cfg: cfg, 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /pkg/dialog/dialog.go: -------------------------------------------------------------------------------- 1 | // package dialog encapsulates standard user messaging for all standard CLI behavior. 2 | // This package is for polished messages that are leveled but unstructured. 3 | // All messages are hidden in json output mode, under the assumption 4 | // that users will want to pipe json output to a file or another process. 5 | package dialog 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "github.com/fatih/color" 13 | "github.com/puppetlabs/relay/pkg/config" 14 | ) 15 | 16 | type Dialog interface { 17 | WithStdout(io.Writer) Dialog 18 | WithStderr(io.Writer) Dialog 19 | 20 | Progress(string) 21 | 22 | Info(string) 23 | Infof(string, ...interface{}) 24 | 25 | Warn(string) 26 | Warnf(string, ...interface{}) 27 | 28 | Error(string) 29 | Errorf(string, ...interface{}) 30 | 31 | WriteString(string) error 32 | 33 | // Table returns a table for formatting for output. 34 | Table() Table 35 | } 36 | 37 | type TextDialog struct { 38 | p *Progress 39 | stdout io.Writer 40 | stderr io.Writer 41 | } 42 | 43 | func (d *TextDialog) WithStdout(w io.Writer) Dialog { 44 | return &TextDialog{stdout: w, stderr: d.stderr, p: d.p} 45 | } 46 | 47 | func (d *TextDialog) WithStderr(w io.Writer) Dialog { 48 | return &TextDialog{stdout: d.stdout, stderr: w, p: d.p} 49 | } 50 | 51 | func withNewLine(str string) string { 52 | if len(str) == 0 { 53 | return "" 54 | } 55 | 56 | if str[len(str)-1] != '\n' { 57 | return str + "\n" 58 | } 59 | 60 | return str 61 | } 62 | 63 | func (d *TextDialog) completeProgress() { 64 | if d.p != nil { 65 | d.p.Complete() 66 | d.p = nil 67 | } 68 | } 69 | 70 | func (d *TextDialog) Info(message string) { 71 | d.completeProgress() 72 | 73 | fmt.Fprintf(d.stdout, withNewLine(message)) 74 | } 75 | 76 | func (d *TextDialog) Infof(message string, args ...interface{}) { 77 | d.completeProgress() 78 | 79 | fmt.Fprintf(d.stdout, withNewLine(message), args...) 80 | } 81 | 82 | func (d *TextDialog) Warn(msg string) { 83 | d.completeProgress() 84 | 85 | fmt.Fprintf(d.stderr, "%s %s", color.YellowString("Warning:"), withNewLine(msg)) 86 | } 87 | 88 | func (d *TextDialog) Warnf(msg string, args ...interface{}) { 89 | d.completeProgress() 90 | 91 | str := fmt.Sprintf(msg, args...) 92 | fmt.Fprintf(d.stderr, "%s %s", color.YellowString("Warning:"), withNewLine(str)) 93 | } 94 | 95 | func (d *TextDialog) Error(msg string) { 96 | d.completeProgress() 97 | 98 | fmt.Fprintf(d.stderr, "%s %s", color.RedString("Error:"), withNewLine(msg)) 99 | } 100 | 101 | func (d *TextDialog) Errorf(msg string, args ...interface{}) { 102 | d.completeProgress() 103 | 104 | str := fmt.Sprintf(msg, args...) 105 | fmt.Fprintf(d.stderr, "%s %s", color.RedString("Error:"), withNewLine(str)) 106 | } 107 | 108 | func (d *TextDialog) Progress(msg string) { 109 | d.completeProgress() 110 | 111 | d.p = NewProgress(d.stdout, msg) 112 | d.p.Start() 113 | } 114 | 115 | func (d *TextDialog) WriteString(c string) error { 116 | _, err := io.WriteString(d.stdout, c) 117 | return err 118 | } 119 | 120 | func (d *TextDialog) Table() Table { 121 | return &textTable{w: d.stdout} 122 | } 123 | 124 | type JSONDialog struct { 125 | stdout, stderr io.Writer 126 | } 127 | 128 | func (d *JSONDialog) WithStdout(w io.Writer) Dialog { 129 | return &JSONDialog{stdout: w, stderr: d.stderr} 130 | } 131 | 132 | func (d *JSONDialog) WithStderr(w io.Writer) Dialog { 133 | return &JSONDialog{stdout: d.stdout, stderr: w} 134 | } 135 | 136 | func (d *JSONDialog) Progress(message string) { 137 | // noop 138 | } 139 | 140 | func (d *JSONDialog) Info(message string) { 141 | // noop 142 | } 143 | 144 | func (d *JSONDialog) Infof(message string, args ...interface{}) { 145 | // noop 146 | } 147 | 148 | func (d *JSONDialog) Warn(msg string) { 149 | fmt.Fprintf(d.stderr, "%s%s", color.YellowString("Warning:"), msg) 150 | } 151 | 152 | func (d *JSONDialog) Warnf(msg string, args ...interface{}) { 153 | str := fmt.Sprintf(msg, args...) 154 | fmt.Fprintf(d.stderr, "%s%s", color.YellowString("Warning:"), str) 155 | } 156 | 157 | func (d *JSONDialog) Error(msg string) { 158 | fmt.Fprintf(d.stderr, "%s%s", color.RedString("Error:"), msg) 159 | } 160 | 161 | func (d *JSONDialog) Errorf(msg string, args ...interface{}) { 162 | str := fmt.Sprintf(msg, args...) 163 | fmt.Fprintf(d.stderr, "%s%s", color.RedString("Error:"), str) 164 | } 165 | 166 | func (d *JSONDialog) WriteString(string) error { 167 | // noop 168 | return nil 169 | } 170 | 171 | func (d *JSONDialog) Table() Table { 172 | return &jsonTable{w: d.stdout} 173 | } 174 | 175 | func FromConfig(cfg *config.Config) Dialog { 176 | switch cfg.Out { 177 | case config.OutputTypeJSON: 178 | return &JSONDialog{stdout: os.Stdout, stderr: os.Stderr} 179 | default: 180 | return &TextDialog{stdout: os.Stdout, stderr: os.Stderr} 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /pkg/dialog/json_table.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | type jsonTable struct { 9 | w io.Writer 10 | headers []string 11 | rows [][]string 12 | } 13 | 14 | func stringsToMap(headers, row []string) map[string]string { 15 | res := make(map[string]string, len(headers)) 16 | 17 | for idx, h := range headers { 18 | res[h] = row[idx] 19 | } 20 | 21 | return res 22 | } 23 | 24 | func allStringsToMap(headers []string, rows [][]string) []map[string]string { 25 | res := make([]map[string]string, 0, len(rows)) 26 | 27 | for _, row := range rows { 28 | res = append(res, stringsToMap(headers, row)) 29 | } 30 | 31 | return res 32 | } 33 | 34 | func (t *jsonTable) Headers(h []string) Table { 35 | t.headers = h 36 | return t 37 | } 38 | 39 | func (t *jsonTable) Rows(rows [][]string) Table { 40 | t.rows = rows 41 | return t 42 | } 43 | 44 | func (t *jsonTable) AppendRow(row []string) Table { 45 | t.rows = append(t.rows, row) 46 | return t 47 | } 48 | 49 | func (t *jsonTable) Flush() error { 50 | return json.NewEncoder(t.w).Encode(allStringsToMap(t.headers, t.rows)) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/dialog/progress.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var chars = []string{ 12 | "⠋", 13 | "⠙", 14 | "⠹", 15 | "⠸", 16 | "⠼", 17 | "⠴", 18 | "⠦", 19 | "⠧", 20 | "⠇", 21 | "⠏", 22 | } 23 | 24 | var ProgressFrameDuration = 250 * time.Millisecond 25 | 26 | // Progress is a text-based progress indicator that has a little animation 27 | // associated with it. It's really cool. 28 | type Progress struct { 29 | c *color.Color 30 | 31 | w io.Writer 32 | msg string 33 | pos int 34 | 35 | // this is the channel that our ticks will come in on from the time package. 36 | // Once it's closed we know we can stop. 37 | ticks *time.Ticker 38 | done chan bool 39 | } 40 | 41 | func (p *Progress) setNextPos() { 42 | p.pos += 1 43 | 44 | if p.pos >= len(chars) { 45 | p.pos = 0 46 | } 47 | } 48 | 49 | func (p *Progress) doRenderFrame() { 50 | fmt.Fprintf(p.w, "%s ", p.msg) 51 | p.c.Fprintf(p.w, "%s\r", chars[p.pos]) 52 | p.setNextPos() 53 | } 54 | 55 | func (p *Progress) doRender() { 56 | for { 57 | select { 58 | case <-p.done: 59 | return 60 | case <-p.ticks.C: 61 | p.doRenderFrame() 62 | } 63 | } 64 | } 65 | 66 | func (p *Progress) Start() { 67 | p.ticks = time.NewTicker(ProgressFrameDuration) 68 | go p.doRender() 69 | } 70 | 71 | func (p *Progress) Complete() { 72 | p.done <- true 73 | 74 | // note that even though the loop has stopped we want to stop the underlying 75 | // ticker. this tells the time package that it can (if need be) be cleaned 76 | // up. 77 | p.ticks.Stop() 78 | 79 | fmt.Fprintf(p.w, "\r%s DONE!\n", p.msg) 80 | } 81 | 82 | func NewProgress(w io.Writer, msg string) *Progress { 83 | c := color.New(color.FgHiMagenta) 84 | 85 | return &Progress{ 86 | c: c, 87 | done: make(chan bool, 0), 88 | w: w, 89 | msg: msg, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/dialog/table.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | type Table interface { 4 | Headers([]string) Table 5 | Rows([][]string) Table 6 | AppendRow([]string) Table 7 | Flush() error 8 | } 9 | -------------------------------------------------------------------------------- /pkg/dialog/text_table.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jedib0t/go-pretty/table" 7 | ) 8 | 9 | type textTable struct { 10 | w io.Writer 11 | headers []string 12 | rows [][]string 13 | } 14 | 15 | func stringsToRow(arr []string) table.Row { 16 | // TODO: Is there a more idiomatic way of converting from []string to []interface? 17 | ifaces := make([]interface{}, 0, len(arr)) 18 | 19 | for _, str := range arr { 20 | ifaces = append(ifaces, str) 21 | } 22 | 23 | return table.Row(ifaces) 24 | } 25 | 26 | func (t *textTable) Headers(h []string) Table { 27 | t.headers = h 28 | return t 29 | } 30 | 31 | func (t *textTable) Rows(rows [][]string) Table { 32 | t.rows = rows 33 | return t 34 | } 35 | 36 | func (t *textTable) AppendRow(row []string) Table { 37 | t.rows = append(t.rows, row) 38 | return t 39 | } 40 | 41 | func (t *textTable) Flush() error { 42 | ta := table.NewWriter() 43 | ta.SetOutputMirror(t.w) 44 | 45 | if t.headers != nil { 46 | ta.AppendHeader(stringsToRow(t.headers)) 47 | } 48 | 49 | if t.rows != nil { 50 | for _, row := range t.rows { 51 | ta.AppendRow(stringsToRow(row)) 52 | } 53 | } 54 | 55 | ta.Render() 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/errors/build.go: -------------------------------------------------------------------------------- 1 | //go:generate go run build_tool.go 2 | 3 | package errors 4 | -------------------------------------------------------------------------------- /pkg/errors/build_tool.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/puppetlabs/errawr-gen/pkg/generator" 9 | ) 10 | 11 | func main() { 12 | err := generator.Generate(generator.Config{ 13 | InputPath: "errors.yaml", 14 | OutputPath: "build_errors.go", 15 | }) 16 | if err != nil { 17 | log.Fatalln(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/errors/errors.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | domain: 3 | key: rcli 4 | title: Relay CLI 5 | sections: 6 | general: 7 | title: General errors 8 | errors: 9 | unknown_error: 10 | title: Unknown error 11 | description: An unexpected error occurred. 12 | config: 13 | title: CLI Config errors 14 | errors: 15 | invalid_config_flag: 16 | title: Invalid config flag 17 | description: Could not read config path. Value must be a valid filepath. 18 | file_not_found: 19 | title: Config file not found 20 | description: No config file found at {{ path }} 21 | arguments: 22 | path: 23 | description: User specified config filepath 24 | invalid_config_file: 25 | title: Invalid config file 26 | description: Could not read config file at {{ path }} 27 | arguments: 28 | path: 29 | description: User specified config filepath 30 | invalid_output_flag: 31 | title: Invalid output flag 32 | description: Unknown value '{{ out }}' provided as output type. Allowed values are 'text' and 'json'. 33 | arguments: 34 | out: 35 | description: User provided output type 36 | invalid_api_domain: 37 | title: Invalid API Domain 38 | description: Provided API Domain {{ domain }} is not a valid url. 39 | arguments: 40 | domain: 41 | description: User provided api domain 42 | invalid_ui_domain: 43 | title: Invalid UI Domain 44 | description: Provided UI Domain {{ domain }} is not a valid url. 45 | arguments: 46 | domain: 47 | description: User provided ui domain 48 | invalid_web_domain: 49 | title: Invalid Web Domain 50 | description: Provided Web Domain {{ domain }} is not a valid url. 51 | arguments: 52 | domain: 53 | description: User provided web domain 54 | client: 55 | title: Client errors 56 | errors: 57 | unknown_error: 58 | title: Unknown error 59 | description: An unexpected error occurred. 60 | # This error probably means a CLI developer screwed up 61 | internal_error: 62 | title: Unknown error 63 | description: There was a problem executing your request. 64 | # This error means there was a problem executing an http request. 65 | request_error: 66 | title: Request error 67 | description: There was a problem executing your request, please try again. 68 | # Used to embed the response body of failed requests. Should always be used as a nested cause 69 | bad_request_body: 70 | title: Bad request error body 71 | sensitivity: "bug" 72 | description: "{{message}}" 73 | arguments: 74 | message: 75 | description: The response body of the failed client request 76 | # Used to embed the response body of failed requests. Should always be used as a nested cause 77 | invalid_encoding_type: 78 | title: Bad request error body 79 | sensitivity: "bug" 80 | description: "{{encoding}} is not a valid encoding type. Valid options are 'json' and 'yaml'." 81 | arguments: 82 | encoding: 83 | description: The provided encoding type 84 | # Generic not found should almost always be replaced with a specific one but it's useful nonetheless 85 | response_not_found: 86 | title: Response not found error 87 | description: Response not found. 88 | user_not_authorized: 89 | title: User not authorized error 90 | description: You are not authorized to perform this operation. If you believe this is a mistake, please contact your Relay administrator. 91 | user_not_authenticated: 92 | title: User not authenticated error 93 | description: You must be logged in to perform this operation. Try `relay auth login`. 94 | command_unavailable_in_client: 95 | title: Command unavailable in client 96 | description: "The command {{command}} is not available in this client. You must use a config context that uses a cluster that supports this command." 97 | arguments: 98 | command: 99 | description: The command that is unavailable 100 | auth: 101 | title: Authentication errors 102 | errors: 103 | failed_login_error: 104 | title: Failed login error 105 | description: Could not log in. Double-check username and password and try again. 106 | failed_pass_from_stdin: 107 | title: Failed password from stdin 108 | description: Could not read password from stdin. 109 | mismatched_email_pass_methods: 110 | title: Mismatched email and password passing methods 111 | description: If you provide a password via --password-stdin you must provide your email as the first positional argument to `relay auth login` 112 | failed_no_stdin: 113 | title: Did not receive from stdin error 114 | description: Did not receive anything from stdin. 115 | workflow: 116 | title: Workflow errors 117 | errors: 118 | workflow_name_read_error: 119 | title: Workflow name read error 120 | description: Could not read workflow name. Please supply a valid name. 121 | workflow_file_read_error: 122 | title: Workflow file read error 123 | description: Could not read workflow file. Check the path to the workflow file. 124 | missing_file_flag_error: 125 | title: Missing file flag error 126 | description: You must specify a workflow file with the --file flag. 127 | missing_name_error: 128 | title: Missing workflow name error 129 | description: Please provide a workflow name. 130 | already_exists_error: 131 | title: Workflow name already exists 132 | description: A workflow with the name provided already exists. Please provide a new name. 133 | does_not_exist_error: 134 | title: Workflow name does not exist 135 | description: A workflow with the name provided does not exist. Please choose an existing workflow. 136 | secret: 137 | title: Secret errors 138 | errors: 139 | name_read_error: 140 | title: Secret name read error 141 | description: Could not read secret name. Please supply a valid name. 142 | missing_name_error: 143 | title: Missing workflow name error 144 | description: Please provide a workflow name. 145 | failed_value_from_stdin: 146 | title: Failed value from stdin 147 | description: Could not read secret value from stdin. 148 | failed_no_stdin: 149 | title: Did not receive from stdin error 150 | description: Did not receive anything from stdin. 151 | -------------------------------------------------------------------------------- /pkg/format/error.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/puppetlabs/errawr-go/v2/pkg/encoding" 9 | "github.com/puppetlabs/relay/pkg/config" 10 | "github.com/puppetlabs/relay/pkg/errors" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func Error(err error, cmd *cobra.Command) string { 15 | // attempt to load config for display options. 16 | cfg, cfgerr := config.FromFlags(cmd.Flags()) 17 | 18 | // if there was a problem loading config use default config 19 | if cfgerr != nil { 20 | cfg = config.GetDefaultConfig() 21 | } 22 | 23 | if cfg.Out == config.OutputTypeJSON { 24 | return formatJSONError(coerceErrawr(err)) 25 | } else { 26 | return formatTextError(coerceErrawr(err), cfg) 27 | } 28 | } 29 | 30 | // coerceErrawr ensures all errors come from errors.yaml as a last-ditch effort 31 | func coerceErrawr(err error) errors.Error { 32 | errawr, ok := err.(errors.Error) 33 | 34 | if ok { 35 | return errawr 36 | } 37 | 38 | return errors.NewGeneralUnknownError().WithCause(err) 39 | } 40 | 41 | // formatJSONError uses errawr envelope encoding to generate a json display of an error 42 | // We could make a condensed json representation but it is very useful to use 43 | // the one we already have for now 44 | func formatJSONError(err errors.Error) string { 45 | display := encoding.ForDisplay(err) 46 | buf, jerr := json.MarshalIndent(display, "", " ") 47 | 48 | if jerr != nil { 49 | panic(jerr) 50 | } 51 | 52 | return string(buf) 53 | } 54 | 55 | func formatTextError(err errors.Error, cfg *config.Config) string { 56 | var out string 57 | 58 | suppressed := appendError(err, cfg, &out, 0, "") 59 | 60 | if cfg.Debug { 61 | out += fmt.Sprintf(` 62 | 63 | You have received an error in debug mode. If the error persists you may file a bug report at https://github.com/puppetlabs/relay/issues.`) 64 | } else if suppressed { 65 | out += fmt.Sprintf(` 66 | 67 | There was a problem executing your request. Rerun with --debug to see more information.`) 68 | } 69 | 70 | return out 71 | } 72 | 73 | // appendError recursively prints errawr causes and items, progressively indented 74 | func appendError(err errors.Error, cfg *config.Config, out *string, indent int, prefix string) (suppressed bool) { 75 | // print error if in debug mode or if Sensitivity is zero 76 | if err.Sensitivity() == 0 || cfg.Debug { 77 | *out += strings.Repeat(" ", indent) 78 | 79 | if prefix != "" { 80 | *out += prefix 81 | } 82 | 83 | *out += err.FormattedDescription().Friendly() 84 | 85 | for _, cause := range err.Causes() { 86 | suppressed = suppressed || appendError(cause, cfg, out, indent+2, "\n• ") 87 | } 88 | 89 | if items, ok := err.Items(); ok { 90 | for itemKey, item := range items { 91 | suppressed = suppressed || appendError(item, cfg, out, indent+2, fmt.Sprintf("\n• `%v`", itemKey)) 92 | } 93 | } 94 | 95 | return suppressed 96 | } 97 | 98 | return true 99 | } 100 | -------------------------------------------------------------------------------- /pkg/format/guilink.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/puppetlabs/relay/pkg/config" 8 | ) 9 | 10 | func GuiLink(cfg *config.Config, path string, a ...interface{}) string { 11 | return cfg.ContextConfig[cfg.CurrentContext].Domains.UIDomain.ResolveReference(&url.URL{Path: fmt.Sprintf(path, a...)}).String() 12 | } 13 | -------------------------------------------------------------------------------- /pkg/model/revision.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/puppetlabs/leg/encoding/transfer" 7 | ) 8 | 9 | type RevisionIdentifier struct { 10 | ID string `json:"id"` 11 | } 12 | 13 | type RevisionSummary struct { 14 | *RevisionIdentifier 15 | } 16 | 17 | type Revision struct { 18 | *RevisionIdentifier 19 | 20 | Parameters WorkflowParameters `json:"parameters"` 21 | Triggers []*WorkflowTrigger `json:"triggers"` 22 | Steps []*WorkflowStep `json:"steps"` 23 | Raw string `json:"raw"` 24 | CreatedAT *time.Time `json:"created_at"` 25 | } 26 | 27 | type RevisionEntity struct { 28 | Revision *Revision `json:"revision"` 29 | } 30 | 31 | type WorkflowParameters map[string]WorkflowParameter 32 | 33 | type WorkflowParameter struct { 34 | Default interface{} `json:"default"` 35 | Description string `json:"description,omitempty"` 36 | } 37 | 38 | type WorkflowTrigger struct { 39 | Name string `json:"name"` 40 | Source *WorkflowTriggerSource `json:"source"` 41 | Binding *WorkflowTriggerBinding `json:"binding"` 42 | } 43 | 44 | type WorkflowTriggerSource struct { 45 | Type string `json:"type"` 46 | PushWorkflowTriggerSource `json:",inline"` 47 | ScheduleWorkflowTriggerSource `json:",inline"` 48 | WebhookWorkflowTriggerSource `json:",inline"` 49 | } 50 | 51 | type PushWorkflowTriggerSource struct { 52 | Schema map[string]transfer.JSONInterface `json:"schema"` 53 | } 54 | 55 | type ScheduleWorkflowTriggerSource struct { 56 | Schedule string `json:"schedule"` 57 | } 58 | 59 | type WebhookWorkflowTriggerSource struct { 60 | ContainerMixin `json:",inline"` 61 | } 62 | 63 | type WorkflowTriggerBinding struct { 64 | Parameters map[string]transfer.JSONInterface `json:"parameters,omitempty"` 65 | } 66 | 67 | type WorkflowStep struct { 68 | Name string `json:"name"` 69 | Description string `json:"description"` 70 | Type string `json:"type"` 71 | DependsOn []string `json:"depends_on,omitempty"` 72 | References *WorkflowDataReferences `json:"references,omitempty"` 73 | ContainerMixin `json:",inline"` 74 | } 75 | 76 | type WorkflowSecretSummary struct { 77 | Name string `json:"name"` 78 | } 79 | 80 | type WorkflowParameterReference struct { 81 | Name string `json:"name"` 82 | } 83 | 84 | type WorkflowOutputReference struct { 85 | Name string `json:"name"` 86 | From string `json:"from"` 87 | } 88 | 89 | type WorkflowDataReferences struct { 90 | Secrets []*WorkflowSecretSummary `json:"secrets,omitempty"` 91 | Parameters []*WorkflowParameterReference `json:"parameters,omitempty"` 92 | Outputs []*WorkflowOutputReference `json:"outputs,omitempty"` 93 | } 94 | 95 | type ContainerMixin struct { 96 | Image string `json:"image,omitempty"` 97 | Spec map[string]transfer.JSONInterface `json:"spec,omitempty"` 98 | Input []string `json:"input,omitempty"` 99 | Command string `json:"command,omitempty"` 100 | Args []string `json:"args,omitempty"` 101 | InputFile string `json:"inputFile,omitempty"` 102 | } 103 | -------------------------------------------------------------------------------- /pkg/model/token.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "fmt" 4 | 5 | type Token string 6 | 7 | func (t *Token) Bearer() string { 8 | return fmt.Sprintf("Bearer %s", t) 9 | } 10 | 11 | func (t Token) String() string { 12 | return string(t) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/model/token_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestToken(t *testing.T) { 10 | token := Token("abc123") 11 | 12 | require.Equal(t, token.Bearer(), "Bearer abc123") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/model/workflow.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/puppetlabs/relay/pkg/config" 9 | ) 10 | 11 | type WorkflowSecretEntity struct { 12 | Secret *WorkflowSecretSummary `json:"secret"` 13 | } 14 | 15 | type WorkflowIdentifier struct { 16 | Name string `json:"name"` 17 | } 18 | 19 | type WorkflowSummary struct { 20 | *WorkflowIdentifier 21 | 22 | Description string `json:"description"` 23 | } 24 | 25 | type Workflow struct { 26 | *WorkflowSummary 27 | 28 | CreatedAt *time.Time `json:"created_at"` 29 | UpdatedAt *time.Time `json:"updated_at"` 30 | LatestRevision *RevisionSummary `json:"latest_revision"` 31 | State *WorkflowState `json:"state,omitempty"` 32 | MostRecentRun WorkflowRun `json:"most_recent_run"` 33 | } 34 | 35 | type WorkflowRun struct { 36 | Revision RevisionSummary `json:"revision"` 37 | RunNumber uint `json:"run_number"` 38 | 39 | // TODO: There's a lot of fields here that are missing. Do we want to backfill them? 40 | } 41 | 42 | type WorkflowEntity struct { 43 | Workflow *Workflow `json:"workflow"` 44 | } 45 | 46 | type WorkflowState struct { 47 | Triggers []*WorkflowTriggerState `json:"triggers"` 48 | } 49 | 50 | type WorkflowTriggerState struct { 51 | Name string `json:"name"` 52 | Revision *RevisionSummary `json:"revision"` 53 | Source *WorkflowTriggerSourceState `json:"source"` 54 | } 55 | 56 | type PushWorkflowTriggerSourceState struct { 57 | Token string `json:"token"` 58 | } 59 | 60 | type ScheduleWorkflowTriggerSourceState struct { 61 | ScheduledAt string `json:"scheduled_at"` 62 | } 63 | 64 | type WebhookWorkflowTriggerSourceState struct { 65 | Endpoint string `json:"endpoint"` 66 | } 67 | 68 | type WorkflowTriggerSourceState struct { 69 | Type string `json:"type"` 70 | Status string `json:"status"` 71 | Push *PushWorkflowTriggerSourceState `json:"push,omitempty"` 72 | Schedule *ScheduleWorkflowTriggerSourceState `json:"schedule,omitempty"` 73 | Webhook *WebhookWorkflowTriggerSourceState `json:"webhook,omitempty"` 74 | } 75 | 76 | // What we call 'workflow' to users is really a combination of these two api types. 77 | // This is a departure from the api spec but feels justified? 78 | type WorkflowRevision struct { 79 | Workflow *Workflow `json:"workflow"` 80 | Revision *Revision `json:"revision"` 81 | } 82 | 83 | func NewWorkflowRevision(workflow *Workflow, revision *Revision) *WorkflowRevision { 84 | return &WorkflowRevision{ 85 | Workflow: workflow, 86 | Revision: revision, 87 | } 88 | } 89 | 90 | func (w *WorkflowRevision) Output(cfg *config.Config) { 91 | if cfg.Out == config.OutputTypeJSON { 92 | w.OutputJSON() 93 | } 94 | // TODO: Text outputter 95 | } 96 | 97 | func (w *WorkflowRevision) OutputJSON() { 98 | jsonBytes, _ := json.MarshalIndent(w, "", " ") 99 | 100 | fmt.Println(string(jsonBytes)) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/util/confirm.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/puppetlabs/relay/pkg/config" 10 | "github.com/puppetlabs/relay/pkg/errors" 11 | ) 12 | 13 | // Confirm prompts users for confirmation, interfacing with global config through --yes flag 14 | func Confirm(prompt string, cfg *config.Config) (bool, errors.Error) { 15 | if cfg.Yes { 16 | return true, nil 17 | } 18 | 19 | reader := bufio.NewReader(os.Stdin) 20 | 21 | fmt.Print(fmt.Sprintf("%v [y/N] ", prompt)) 22 | prompt, err := reader.ReadString('\n') 23 | 24 | if err != nil { 25 | return false, errors.NewGeneralUnknownError().WithCause(err) 26 | } 27 | 28 | return strings.Contains(strings.TrimSpace(prompt), "y"), nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/util/stdin.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | ) 8 | 9 | func PassedStdin() (bool, error) { 10 | info, err := os.Stdin.Stat() 11 | if err != nil { 12 | return false, err 13 | } 14 | 15 | if (info.Mode() & os.ModeCharDevice) == 0 { 16 | return true, nil 17 | } 18 | 19 | return false, nil 20 | } 21 | 22 | func ReadStdin(readLimit int64) ([]byte, error) { 23 | buf := bytes.Buffer{} 24 | reader := &io.LimitedReader{R: os.Stdin, N: readLimit} 25 | 26 | n, err := buf.ReadFrom(reader) 27 | if err != nil && err != io.EOF { 28 | return nil, err 29 | } 30 | if n == 0 { 31 | return nil, nil 32 | } 33 | 34 | return buf.Bytes(), nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "runtime/debug" 4 | 5 | var Version string 6 | 7 | func GetVersion() string { 8 | if Version == "" { 9 | i, ok := debug.ReadBuildInfo() 10 | if !ok { 11 | return "" 12 | } 13 | Version = i.Main.Version 14 | } 15 | return Version 16 | } 17 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # 5 | # Commands 6 | # 7 | 8 | MKDIR_P="${MKDIR_P:-mkdir -p}" 9 | GO="${GO:-go}" 10 | 11 | # 12 | # Variables 13 | # 14 | 15 | BIN_DIR="${BIN_DIR:-bin}" 16 | 17 | GOOS="$( $GO env GOOS )" 18 | GOARCH="$( $GO env GOARCH )" 19 | LDFLAGS="${LDFLAGS:-}" 20 | 21 | # 22 | # 23 | # 24 | 25 | . scripts/library.sh 26 | 27 | eval "$( relay::cli::cli_vars )" 28 | 29 | VERSION_STR="${CLI_VERSION} (`date -u -R`)" 30 | 31 | $MKDIR_P "${BIN_DIR}" 32 | 33 | set -x 34 | $GO build -o "${BIN_DIR}/${CLI_FILE_BIN}" -ldflags "-X \"github.com/puppetlabs/relay/pkg/version.Version=${VERSION_STR}\" ${LDFLAGS[*]}" "./cmd/${CLI_NAME}" 35 | 36 | relay::cli::sha256sum < "${BIN_DIR}/${CLI_FILE_BIN}" > "${BIN_DIR}/${CLI_FILE_BIN}.sha256" 37 | -------------------------------------------------------------------------------- /scripts/ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | if [ -n "${TRAVIS_TAG-}" ]; then 5 | export GIT_TAG_OVERRIDE="${TRAVIS_TAG}" 6 | fi 7 | 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /scripts/dist: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # 5 | # Commands 6 | # 7 | 8 | MKDIR_P="${MKDIR_P:-mkdir -p}" 9 | GO="${GO:-go}" 10 | TAR="${TAR:-tar}" 11 | ZIP_M="${ZIP_M:-zip -m}" 12 | 13 | # 14 | # Variables 15 | # 16 | 17 | GOOS="$( $GO env GOOS )" 18 | GOARCH="$( $GO env GOARCH )" 19 | 20 | # 21 | # 22 | # 23 | 24 | . scripts/library.sh 25 | 26 | relay::cli::release_check 27 | eval "$( relay::cli::cli_vars )" 28 | 29 | ARTIFACTS_CLI_DIR="bin" 30 | 31 | BIN_DIR="${ARTIFACTS_CLI_DIR}" \ 32 | CGO_ENABLED=0 \ 33 | GOFLAGS="${GOFLAGS:-} -a" \ 34 | LDFLAGS="${LDFLAGS:-}"' -extldflags "-static"' \ 35 | scripts/build "${CLI_NAME}" 36 | 37 | case "$( $GO env GOOS )" in 38 | windows) 39 | ( 40 | set -x 41 | pushd "${ARTIFACTS_CLI_DIR}" >/dev/null 42 | relay::cli::sha256sum <"${CLI_FILE_PREFIX}.exe" >"${CLI_FILE_PREFIX}.exe.sha256" 43 | ) 44 | ;; 45 | *) 46 | ( 47 | set -x 48 | pushd "${ARTIFACTS_CLI_DIR}" >/dev/null 49 | relay::cli::sha256sum <"${CLI_FILE_PREFIX}" >"${CLI_FILE_PREFIX}.sha256" 50 | ) 51 | ;; 52 | esac 53 | 54 | set +x 55 | -------------------------------------------------------------------------------- /scripts/dist-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | . scripts/library.sh 5 | 6 | GO_DIST_OS_ARCHES=( linux-amd64 windows-amd64 darwin-amd64 ) 7 | 8 | for OS_ARCH in "${GO_DIST_OS_ARCHES[@]}"; do 9 | echo "# dist (go): relay ${OS_ARCH}" 10 | GOOS="${OS_ARCH%-*}" GOARCH="${OS_ARCH##*-}" scripts/dist 11 | done 12 | -------------------------------------------------------------------------------- /scripts/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # 5 | # Commands 6 | # 7 | 8 | GO="${GO:-go}" 9 | 10 | # 11 | # 12 | # 13 | 14 | set -x 15 | $GO generate ./... -------------------------------------------------------------------------------- /scripts/library.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Commands 5 | # 6 | 7 | FIND="${FIND:-find}" 8 | GIT="${GIT:-git}" 9 | GSUTIL="${GSUTIL:-gsutil}" 10 | SHA256SUM="${SHA256SUM:-shasum -a 256}" 11 | 12 | # 13 | # 14 | # 15 | 16 | relay::cli::default_programs() { 17 | local DEFAULT_PROGRAMS 18 | DEFAULT_PROGRAMS=( relay ) 19 | 20 | for DEFAULT_PROGRAM in ${DEFAULT_PROGRAMS[@]}; do 21 | printf "%s\n" "${DEFAULT_PROGRAM}" 22 | done 23 | } 24 | 25 | relay::cli::git_tag() { 26 | printf "%s\n" "${GIT_TAG_OVERRIDE:-$( $GIT tag --points-at HEAD 'v*.*.*' )}" 27 | } 28 | 29 | relay::cli::sha256sum() { 30 | $SHA256SUM | cut -d' ' -f1 31 | } 32 | 33 | relay::cli::escape_shell() { 34 | printf '%s\n' "'${*//\'/\'\"\'\"\'}'" 35 | } 36 | 37 | relay::cli::release_version() { 38 | local GIT_TAG GIT_CHANGED_FILES 39 | GIT_TAG="$( relay::cli::git_tag )" 40 | GIT_CHANGED_FILES="$( $GIT status --short )" 41 | 42 | # Check for releasable version: if we have no tags or any changed files, we 43 | # can't release. 44 | if [ -z "${GIT_TAG}" ] || [ -n "${GIT_CHANGED_FILES}" ]; then 45 | return 1 46 | fi 47 | 48 | # Arbitrarily pick the first line. 49 | read GIT_TAG_A <<<"${GIT_TAG}" 50 | 51 | printf "%s\n" "${GIT_TAG_A#v}" 52 | } 53 | 54 | relay::cli::release_check() { 55 | if ! relay::cli::release_version >/dev/null; then 56 | echo "$0: no release tag (this commit must be tagged with the format vX.Y.Z)" >&2 57 | return 2 58 | fi 59 | } 60 | 61 | relay::cli::release_vars() { 62 | RELEASE_VERSION="$( relay::cli::release_version || true )" 63 | if [ -z "${RELEASE_VERSION}" ]; then 64 | printf 'RELEASE_VERSION=\n' 65 | return 66 | fi 67 | 68 | # Parse the version information. 69 | IFS='.' read RELEASE_VERSION_MAJOR RELEASE_VERSION_MINOR RELEASE_VERSION_PATCH <<<"${RELEASE_VERSION}" 70 | 71 | printf 'RELEASE_VERSION=%s\n' "$( relay::cli::escape_shell "${RELEASE_VERSION}" )" 72 | printf 'RELEASE_VERSION_MAJOR=%s\n' "$( relay::cli::escape_shell "${RELEASE_VERSION_MAJOR}" )" 73 | printf 'RELEASE_VERSION_MINOR=%s\n' "$( relay::cli::escape_shell "${RELEASE_VERSION_MINOR}" )" 74 | printf 'RELEASE_VERSION_PATCH=%s\n' "$( relay::cli::escape_shell "${RELEASE_VERSION_PATCH}" )" 75 | } 76 | 77 | relay::cli::release_vars_local() { 78 | printf 'local RELEASE_VERSION RELEASE_VERSION_MAJOR RELEASE_VERSION_MINOR RELEASE_VERSION_PATCH\n' 79 | relay::cli::release_vars "$@" 80 | } 81 | 82 | relay::cli::release() { 83 | if [[ "$#" -lt 2 ]]; then 84 | echo "usage: ${FUNCNAME[0]} [dist-ext [dist-prefix]]" >&2 85 | return 1 86 | fi 87 | 88 | relay::cli::release_check 89 | eval "$( relay::cli::release_vars )" 90 | 91 | } 92 | 93 | relay::cli::version() { 94 | eval "$( relay::cli::release_vars )" 95 | 96 | if [ -n "${RELEASE_VERSION}" ]; then 97 | printf "%s\n" "v${RELEASE_VERSION}" 98 | else 99 | $GIT describe --tags --always --dirty 100 | fi 101 | } 102 | 103 | relay::cli::cli_vars() { 104 | local GO GOOS GOARCH 105 | GO="${GO:-go}" 106 | GOOS="$( $GO env GOOS )" 107 | GOARCH="$( $GO env GOARCH )" 108 | 109 | local EXT= 110 | [[ "${GOOS}" == "windows" ]] && EXT=.exe 111 | 112 | printf 'CLI_NAME=%s\n' "$( echo relay )" 113 | printf 'CLI_VERSION=%s\n' "$( relay::cli::version )" 114 | printf 'CLI_FILE_PREFIX="${CLI_NAME}-${CLI_VERSION}"-%s-%s\n' \ 115 | "$( relay::cli::escape_shell "${GOOS}" )" \ 116 | "$( relay::cli::escape_shell "${GOARCH}" )" 117 | printf 'CLI_FILE_BIN="${CLI_FILE_PREFIX}%s"\n' "${EXT}" 118 | } 119 | 120 | relay::cli::cli_vars_local() { 121 | printf 'local CLI_NAME CLI_FILE_PREFIX CLI_FILE_BIN\n' 122 | relay::cli::cli_vars "$@" 123 | } 124 | 125 | relay::cli::cli_artifacts() { 126 | if [[ "$#" -ne 2 ]]; then 127 | echo "usage: ${FUNCNAME[0]} " >&2 128 | return 1 129 | fi 130 | 131 | eval "$( relay::cli::cli_vars_local "$1" )" 132 | 133 | local CLI_MATCH 134 | CLI_MATCH="${CLI_NAME}-${CLI_VERSION}-" 135 | 136 | $FIND "$2" -type f -name "${CLI_MATCH}"'*' 137 | } 138 | 139 | relay::cli::cli_platform_ext() { 140 | if [[ "$#" -ne 2 ]]; then 141 | echo "usage: ${FUNCNAME[0]} " >&2 142 | return 1 143 | fi 144 | 145 | eval "$( relay::cli::cli_vars_local "$1" )" 146 | 147 | local CLI_FILE 148 | CLI_FILE="$( basename "$2" )" 149 | 150 | printf "%s\n" "${CLI_FILE##${CLI_NAME}-${CLI_VERSION}-}" 151 | } 152 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | # all this needs to do is create the sha256 in the right place 5 | 6 | ARTIFACTS_DIR="${ARTIFACTS_DIR:-bin}" 7 | 8 | # 9 | # 10 | # 11 | 12 | . scripts/library.sh 13 | 14 | [[ "$#" -eq 1 ]] || echo "usage: $0 " 15 | 16 | eval "$( relay::cli::cli_vars "$1" )" 17 | 18 | for PACKAGE in $( relay::cli::cli_artifacts "$1" "${ARTIFACTS_DIR}" ); do 19 | # Get the remaining file path and extension (directory, name and version 20 | # truncated) from the package. 21 | CLI_PLATFORM_EXT="-$( relay::cli::cli_platform_ext "$1" "${PACKAGE}" )" 22 | relay::cli::release "${CLI_NAME}" "${PACKAGE}" "${CLI_PLATFORM_EXT}" 23 | relay::cli::release "${CLI_NAME}" "${PACKAGE}.sha256" "${CLI_PLATFORM_EXT}.sha256" 24 | done 25 | -------------------------------------------------------------------------------- /scripts/release-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | . scripts/library.sh 5 | 6 | RELEASE_PROGRAMS=( $( relay::cli::default_programs ) ) 7 | 8 | for PROGRAM in "${RELEASE_PROGRAMS[@]}"; do 9 | echo "# release (go): ${PROGRAM}" 10 | scripts/release "${PROGRAM}" 11 | done 12 | -------------------------------------------------------------------------------- /scripts/run-workflow: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | TAG=${TRAVIS_TAG} 5 | CLI="bin/relay-${TAG}-linux-amd64" 6 | SHA=$(<"bin/relay-${TAG}-darwin-amd64.sha256") 7 | 8 | chmod +x $CLI 9 | 10 | echo -n "${RELAY_OPERATIONS_API_TOKEN}" | ${CLI} auth login --stdin 11 | 12 | ${CLI} workflow run --parameter tag=${TAG} --parameter sha=${SHA} relay-cli-update-brew 13 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # 5 | # Commands 6 | # 7 | 8 | GO="${GO:-go}" 9 | 10 | # 11 | # 12 | # 13 | 14 | ( 15 | set -x 16 | $GO test ./... 17 | ) -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //+build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/go-swagger/go-swagger/cmd/swagger" 7 | _ "github.com/google/wire/cmd/wire" 8 | _ "github.com/shurcooL/vfsgen" 9 | ) 10 | --------------------------------------------------------------------------------