├── .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 |
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 |
--------------------------------------------------------------------------------