├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .leewayignore ├── .vscode └── launch.json ├── .werft ├── build-job.yaml ├── config.yaml ├── debug.yaml ├── node-debug.js └── node-debug.yaml ├── BUILD.yaml ├── LICENSE ├── README.md ├── WORKSPACE.yaml ├── client.go ├── cmd ├── client │ ├── init-job.go │ ├── init.go │ ├── job-get.go │ ├── job-list.go │ ├── job-logs.go │ ├── job-stop.go │ ├── job.go │ ├── log-phase.go │ ├── log-result.go │ ├── log-slice.go │ ├── log.go │ ├── root.go │ ├── run-github.go │ ├── run-local.go │ ├── run-previous.go │ ├── run.go │ └── version.go └── server │ ├── root.go │ ├── run.go │ └── version.go ├── go.mod ├── go.sum ├── helm ├── Chart.yaml ├── charts │ └── postgresql-8.1.2.tgz ├── requirements.lock ├── requirements.yaml ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── secret.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── statefulset.yaml └── values.yaml ├── logo.png ├── logo.svg ├── logo_background.svg ├── logo_path.svg ├── pkg ├── api │ ├── repoconfig │ │ ├── repoconfig.go │ │ └── repoconfig_test.go │ └── v1 │ │ ├── generate.sh │ │ ├── mock │ │ └── mock.go │ │ ├── werft-ui.pb.go │ │ ├── werft-ui.proto │ │ ├── werft.pb.go │ │ └── werft.proto ├── auth │ ├── auth.go │ └── opa.go ├── executor │ ├── executor.go │ ├── labels.go │ ├── loglistener.go │ └── status.go ├── filterexpr │ ├── filterexpr.go │ └── filterexpr_test.go ├── logcutter │ ├── logcutter.go │ └── logcutter_test.go ├── plugin │ ├── client │ │ └── client.go │ ├── common │ │ ├── auth-plugin.pb.go │ │ ├── auth-plugin.proto │ │ ├── common.go │ │ ├── generate.sh │ │ ├── repo-plugin.pb.go │ │ └── repo-plugin.proto │ └── host │ │ ├── host.go │ │ └── repo.go ├── prettyprint │ ├── json.go │ ├── prettyprint.go │ ├── template.go │ └── yaml.go ├── reporef │ └── reporef.go ├── store │ ├── logfile.go │ ├── logfile_test.go │ ├── memory.go │ ├── postgres │ │ ├── BUILD.yaml │ │ ├── job.go │ │ ├── leeway.go │ │ ├── migration.go │ │ ├── migrations │ │ │ ├── 20191219172119_initial-schema.down.sql │ │ │ ├── 20191219172119_initial-schema.up.sql │ │ │ ├── 20191219180328_indices.down.sql │ │ │ ├── 20191219180328_indices.up.sql │ │ │ ├── 20191229110336_job-spec.down.sql │ │ │ ├── 20191229110336_job-spec.up.sql │ │ │ ├── 20200119172146_did_execute.down.sql │ │ │ ├── 20200119172146_did_execute.up.sql │ │ │ ├── 20220220151605_job-spec-add.down.sql │ │ │ └── 20220220151605_job-spec-add.up.sql │ │ └── numbergroup.go │ └── store.go ├── version │ └── version.go ├── webui │ ├── .env │ ├── .eslintignore │ ├── .gitignore │ ├── BUILD.yaml │ ├── README.md │ ├── generate.go │ ├── package.json │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-failure-16x16.png │ │ ├── favicon-failure-32x32.png │ │ ├── favicon-failure.ico │ │ ├── favicon-success-16x16.png │ │ ├── favicon-success-32x32.png │ │ ├── favicon-success.ico │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── robots.txt │ │ ├── safari-pinned-tab.svg │ │ ├── site.webmanifest │ │ └── werft-small.png │ ├── src │ │ ├── App.tsx │ │ ├── BranchList.tsx │ │ ├── GithubPage.tsx │ │ ├── JobList.tsx │ │ ├── JobTree.tsx │ │ ├── JobView.tsx │ │ ├── StartJob.tsx │ │ ├── api │ │ │ ├── werft-ui_pb.d.ts │ │ │ ├── werft-ui_pb.js │ │ │ ├── werft-ui_pb_service.d.ts │ │ │ ├── werft-ui_pb_service.js │ │ │ ├── werft_pb.d.ts │ │ │ ├── werft_pb.js │ │ │ ├── werft_pb_service.d.ts │ │ │ └── werft_pb_service.js │ │ ├── components │ │ │ ├── IsReadonly.tsx │ │ │ ├── LogView.tsx │ │ │ ├── Naviagor.tsx │ │ │ ├── ResultView.tsx │ │ │ ├── SearchBox.tsx │ │ │ ├── StickyScroll.tsx │ │ │ ├── colors.ts │ │ │ ├── header.tsx │ │ │ ├── terminal.css │ │ │ └── util.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── registerServiceWorker.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock └── werft │ ├── content.go │ ├── service.go │ ├── service_test.go │ ├── uiservice.go │ └── werft.go ├── plugins ├── BUILD.yaml ├── cron │ ├── BUILD.yaml │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── github-auth │ ├── .gitignore │ ├── BUILD.yaml │ ├── go.mod │ ├── go.sum │ └── main.go ├── github-integration │ ├── .gitignore │ ├── BUILD.yaml │ ├── README.md │ ├── docs │ │ └── check-screenshot.png │ ├── fixtures │ │ ├── handleCommandRun_args.golden │ │ ├── handleCommandRun_args.json │ │ ├── handleCommandRun_defaultbranch.golden │ │ ├── handleCommandRun_defaultbranch.json │ │ ├── handleCommandRun_success.golden │ │ ├── handleCommandRun_success.json │ │ ├── processPullRequestEvent_noMD.golden │ │ ├── processPullRequestEvent_noMD.json │ │ ├── processPullRequestEvent_noRerun.golden │ │ ├── processPullRequestEvent_noRerun.json │ │ ├── processPullRequestEvent_rerun.golden │ │ ├── processPullRequestEvent_rerun.json │ │ ├── processPullRequestEvent_withMD.golden │ │ ├── processPullRequestEvent_withMD.json │ │ ├── processPushEvent_delete.golden │ │ ├── processPushEvent_delete.json │ │ ├── processPushEvent_push.golden │ │ └── processPushEvent_push.json │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── main_test.go ├── github-repo │ ├── .gitignore │ ├── BUILD.yaml │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── pkg │ │ └── provider │ │ ├── provider.go │ │ └── provider_test.go ├── integration-example │ ├── BUILD.yaml │ ├── go.mod │ ├── go.sum │ └── main.go ├── otel-exporter │ ├── BUILD.yaml │ ├── README.md │ ├── example-log.txt │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── main_test.go └── webhook │ ├── BUILD.yaml │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── prepare.sh ├── release.sh ├── server.Dockerfile ├── server.go ├── testdata ├── credential-helper.sh ├── example-app.pem ├── example-config.yaml ├── in-gitpod-config.yaml ├── policy │ └── api.rego ├── push-event-payload.json └── send-push-event.sh └── werft.theia-workspace /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | **/rice-box.go 3 | .idea -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-postgres 2 | ARG TRIGGER_REBUILD=1 3 | 4 | USER root 5 | 6 | ENV PROTOC_ZIP=protoc-3.7.1-linux-x86_64.zip 7 | RUN curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.7.1/$PROTOC_ZIP && \ 8 | unzip -o $PROTOC_ZIP -d /usr/local bin/protoc && \ 9 | unzip -o $PROTOC_ZIP -d /usr/local 'include/*' && \ 10 | rm -f $PROTOC_ZIP 11 | 12 | RUN curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh 13 | RUN curl -L https://download.docker.com/linux/static/stable/x86_64/docker-19.03.5.tgz | tar xz && \ 14 | mv docker/docker /usr/bin && \ 15 | rm -rf docker 16 | 17 | RUN curl -o /usr/bin/k3s -L https://github.com/rancher/k3s/releases/download/v1.0.1/k3s && \ 18 | chmod +x /usr/bin/k3s 19 | 20 | ENV LEEWAY_WORKSPACE_ROOT=/workspace/werft 21 | RUN curl -L https://github.com/TypeFox/leeway/releases/download/v0.2.17/leeway_0.2.17_Linux_x86_64.tar.gz | tar xz && \ 22 | mv leeway /usr/bin/leeway && \ 23 | rm README.md 24 | 25 | RUN curl -L https://get.helm.sh/helm-v3.3.0-rc.2-linux-amd64.tar.gz | tar xz linux-amd64/helm && \ 26 | mv linux-amd64/helm /usr/bin && \ 27 | rm -r linux-amd64 28 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - init: go get && go build ./... && go test ./... 5 | command: | 6 | echo $SECRET_KUBECONFIG | base64 -d > ~/.kube/config 7 | sleep 10 8 | echo CREATE DATABASE werft | psql -f - 9 | mkdir /tmp/logs 10 | find . -name "go.mod" | while read f; do echo cd $(dirname $f); echo go get -v ./...; echo cd -; done | sh 11 | unset PGHOSTADDR 12 | gp await-port 3000 13 | go run server.go run testdata/in-gitpod-config.yaml --debug-webui-proxy http://localhost:3000 14 | - init: yarn --cwd pkg/webui 15 | command: yarn --cwd pkg/webui start 16 | openMode: split-right 17 | - command: k3s server --disable-agent 18 | openMode: split-right 19 | ports: 20 | - port: 6443 21 | - port: 8080 22 | onOpen: open-preview 23 | - port: 2999-3001 24 | onOpen: ignore 25 | workspaceLocation: werft/werft.theia-workspace 26 | vscode: 27 | extensions: 28 | - zxh404.vscode-proto3 29 | - golang.go -------------------------------------------------------------------------------- /.leewayignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": { 14 | "CGO_ENABLED": "0" 15 | }, 16 | "args": [] 17 | }, 18 | { 19 | "name": "Debug", 20 | "type": "go", 21 | "request": "launch", 22 | "mode": "debug", 23 | "program": "${workspaceFolder}/server.go", 24 | "env": { 25 | "CGO_ENABLED": "0" 26 | }, 27 | "args": [ 28 | "run", 29 | "--verbose=true", 30 | "--debug-webui-proxy=http://localhost:3000", 31 | "testdata/in-gitpod-config.yaml" 32 | ] 33 | }, 34 | { 35 | "name": "Debug Job Log", 36 | "type": "go", 37 | "request": "launch", 38 | "mode": "debug", 39 | "program": "${workspaceFolder}/client.go", 40 | "env": { 41 | "CGO_ENABLED": "0" 42 | }, 43 | "buildFlags": "-tags client", 44 | "args": [ 45 | "job", 46 | "logs", 47 | "werft-build-job-test-branches.1" 48 | ] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /.werft/build-job.yaml: -------------------------------------------------------------------------------- 1 | mutex: build-{{ .Repository.Revision }} 2 | pod: 3 | containers: 4 | - name: build 5 | image: golang:1.13-alpine 6 | workingDir: /workspace 7 | imagePullPolicy: IfNotPresent 8 | command: 9 | - sh 10 | - -c 11 | - | 12 | apk add --no-cache sed curl go yarn git coreutils 13 | curl -L https://github.com/TypeFox/leeway/releases/download/v0.1.0/leeway_0.1.0_Linux_x86_64.tar.gz | tar xz 14 | chmod +x leeway 15 | export PATH=$PWD:$PATH 16 | cd /workspace 17 | echo "[build|PHASE] build" 18 | leeway build --werft -Dversion={{ .Name }} -Dcommit={{ .Repository.Revision }} -Ddate="$(date)" -------------------------------------------------------------------------------- /.werft/config.yaml: -------------------------------------------------------------------------------- 1 | defaultJob: ".werft/build-job.yaml" -------------------------------------------------------------------------------- /.werft/debug.yaml: -------------------------------------------------------------------------------- 1 | pod: 2 | containers: 3 | - name: build 4 | image: alpine:latest 5 | workingDir: /workspace 6 | imagePullPolicy: IfNotPresent 7 | command: 8 | - sh 9 | - -c 10 | - | 11 | sleep 5 12 | echo "[build|PHASE] building stuff" 13 | for i in $(seq 1 10); do sleep 2; echo "[foo] output $i"; done 14 | echo "[url|RESULT] https://github.com/csweichel/werft the github project" 15 | echo "[url|RESULT] https://github.com/csweichel/tree/{{ .Repository.Ref }} this branch on Github" 16 | echo "hello world" 17 | echo "some more regular logging" 18 | echo "{{ .Annotations.msg }}" 19 | echo "[docker|RESULT] csweichel/werft:{{ .Name }} this version's docker image" 20 | echo "[docker|RESULT] csweichel/werft-utils:{{ .Name }} this versions utility image" 21 | -------------------------------------------------------------------------------- /.werft/node-debug.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const fs = require('fs'); 3 | 4 | const context = JSON.parse(fs.readFileSync('context.json')); 5 | console.log(context); -------------------------------------------------------------------------------- /.werft/node-debug.yaml: -------------------------------------------------------------------------------- 1 | pod: 2 | containers: 3 | - name: node-debug 4 | image: node:13-alpine 5 | workingDir: /workspace 6 | imagePullPolicy: IfNotPresent 7 | command: 8 | - sh 9 | - -c 10 | - | 11 | npm install shelljs 12 | printf '{{ toJson . }}' > context.json 13 | node --inspect .werft/node-debug.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2019 Christian Weichel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /WORKSPACE.yaml: -------------------------------------------------------------------------------- 1 | defaultTarget: //:all 2 | defaultVariant: 3 | config: 4 | go: 5 | lintCommand: ["sh", "-c", "golangci-lint run --disable govet,errcheck,typecheck,staticcheck --allow-parallel-runners --timeout 5m"] -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | //go:build client 2 | // +build client 3 | 4 | // Copyright © 2019 Christian Weichel 5 | 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | package main 25 | 26 | import ( 27 | cmd "github.com/csweichel/werft/cmd/client" 28 | 29 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 30 | ) 31 | 32 | func main() { 33 | cmd.Execute() 34 | } 35 | -------------------------------------------------------------------------------- /cmd/client/init-job.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "fmt" 25 | "io/ioutil" 26 | "os" 27 | "path/filepath" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var initJobCmd = &cobra.Command{ 33 | Use: "job ", 34 | Short: "creates a job YAML file", 35 | Args: cobra.ExactArgs(1), 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | name := args[0] 38 | fn, _ := cmd.Flags().GetString("output") 39 | if fn == "" { 40 | fn = filepath.Join(".werft", fmt.Sprintf("%s.yaml", name)) 41 | } 42 | 43 | if _, err := os.Stat(filepath.Dir(fn)); err != nil { 44 | err := os.MkdirAll(filepath.Dir(fn), 0755) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | 50 | return ioutil.WriteFile(fn, []byte(`pod: 51 | containers: 52 | - name: `+name+` 53 | image: alpine:latest 54 | workingDir: /workspace 55 | imagePullPolicy: IfNotPresent 56 | command: 57 | - sh 58 | - -c 59 | - | 60 | echo Hello World`), 0644) 61 | }, 62 | } 63 | 64 | func init() { 65 | initCmd.AddCommand(initJobCmd) 66 | 67 | initJobCmd.Flags().StringP("output", "o", "", "output filename (defaults to .werft/jobname.yaml)") 68 | } 69 | -------------------------------------------------------------------------------- /cmd/client/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // initCmd represents the job command 28 | var initCmd = &cobra.Command{ 29 | Use: "init", 30 | Short: "Initializes configuration for werft", 31 | Args: cobra.ExactArgs(1), 32 | } 33 | 34 | func init() { 35 | rootCmd.AddCommand(initCmd) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/client/job-get.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | v1 "github.com/csweichel/werft/pkg/api/v1" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var jobGetTpl = `Name: {{ .Name }} 29 | Phase: {{ .Phase }} 30 | Success: {{ .Conditions.Success }} 31 | Metadata: 32 | Owner: {{ .Metadata.Owner }} 33 | Trigger: {{ .Metadata.Trigger }} 34 | Started: {{ .Metadata.Created | toRFC3339 }} 35 | Finished: {{ .Metadata.Finished | toRFC3339 }} 36 | Repository: 37 | Host: {{ .Metadata.Repository.Host }} 38 | Owner: {{ .Metadata.Repository.Owner }} 39 | Repo: {{ .Metadata.Repository.Repo }} 40 | Ref: {{ .Metadata.Repository.Ref }} 41 | Revision: {{ .Metadata.Repository.Revision }} 42 | {{- if .Results }} 43 | Results: 44 | {{- range .Results }} 45 | {{ .Type }}: {{ .Payload }} 46 | {{ .Description -}} 47 | {{ end -}} 48 | {{- end }} 49 | ` 50 | 51 | // jobGetCmd represents the list command 52 | var jobGetCmd = &cobra.Command{ 53 | Use: "get [name]", 54 | Short: "Retrieves details of a job", 55 | Args: cobra.MaximumNArgs(1), 56 | RunE: func(cmd *cobra.Command, args []string) error { 57 | conn := dial() 58 | defer conn.Close() 59 | client := v1.NewWerftServiceClient(conn) 60 | 61 | name, localJobContext, err := getLocalJobName(client, args) 62 | if err != nil { 63 | return err 64 | } 65 | ctx, cancel, err := getRequestContext(localJobContext) 66 | if err != nil { 67 | return err 68 | } 69 | defer cancel() 70 | 71 | resp, err := client.GetJob(ctx, &v1.GetJobRequest{ 72 | Name: name, 73 | }) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return prettyPrint(resp.Result, jobGetTpl) 79 | }, 80 | } 81 | 82 | func init() { 83 | jobCmd.AddCommand(jobGetCmd) 84 | } 85 | -------------------------------------------------------------------------------- /cmd/client/job-logs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "os" 27 | 28 | v1 "github.com/csweichel/werft/pkg/api/v1" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // jobLogsCmd represents the list command 33 | var jobLogsCmd = &cobra.Command{ 34 | Use: "logs [name]", 35 | Short: "Listens to the log output of a job", 36 | Args: cobra.MaximumNArgs(1), 37 | RunE: func(cmd *cobra.Command, args []string) error { 38 | conn := dial() 39 | defer conn.Close() 40 | client := v1.NewWerftServiceClient(conn) 41 | 42 | name, localJobContext, err := getLocalJobName(client, args) 43 | if err != nil { 44 | return err 45 | } 46 | ctx, cancel, err := getRequestContext(localJobContext) 47 | if err != nil { 48 | return err 49 | } 50 | defer cancel() 51 | 52 | fmt.Printf("showing logs of \033[34m\033[1m%s\t\033\033[0m\n", name) 53 | 54 | return followJob(ctx, client, name, "") 55 | }, 56 | } 57 | 58 | func followJob(ctx context.Context, client v1.WerftServiceClient, name, prefix string) error { 59 | logs, err := client.Listen(ctx, &v1.ListenRequest{ 60 | Name: name, 61 | Logs: v1.ListenRequestLogs_LOGS_RAW, 62 | Updates: true, 63 | }) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for { 69 | msg, err := logs.Recv() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | if update := msg.GetUpdate(); update != nil { 75 | if update.Phase == v1.JobPhase_PHASE_DONE { 76 | prettyPrint(update, jobGetTpl) 77 | 78 | if update.Conditions.Success { 79 | os.Exit(0) 80 | } else { 81 | os.Exit(1) 82 | } 83 | } 84 | } 85 | if data := msg.GetSlice(); data != nil { 86 | if prefix == "" { 87 | pringLogSlice(data) 88 | } else { 89 | printLogSliceWithPrefix(prefix, data) 90 | } 91 | } 92 | } 93 | } 94 | 95 | func pringLogSlice(slice *v1.LogSliceEvent) { 96 | if slice.Name == "werft:kubernetes" || slice.Name == "werft:status" { 97 | return 98 | } 99 | 100 | var tpl string 101 | switch slice.Type { 102 | case v1.LogSliceType_SLICE_PHASE: 103 | tpl = "\033[33m\033[1m{{ .Name }}\t\033[39m{{ .Payload }}\033[0m\n" 104 | case v1.LogSliceType_SLICE_CONTENT: 105 | tpl = "\033[2m[{{ .Name }}]\033[0m {{ .Payload }}\n" 106 | } 107 | if tpl == "" { 108 | return 109 | } 110 | prettyPrint(slice, tpl) 111 | } 112 | 113 | func printLogSliceWithPrefix(prefix string, slice *v1.LogSliceEvent) { 114 | if slice.Name == "werft:kubernetes" || slice.Name == "werft:status" { 115 | return 116 | } 117 | 118 | switch slice.Type { 119 | case v1.LogSliceType_SLICE_PHASE: 120 | fmt.Printf("[%s%s|PHASE] %s\n", prefix, slice.Name, slice.Payload) 121 | case v1.LogSliceType_SLICE_CONTENT: 122 | fmt.Printf("[%s%s] %s\n", prefix, slice.Name, slice.Payload) 123 | case v1.LogSliceType_SLICE_DONE: 124 | fmt.Printf("[%s%s|DONE] %s\n", prefix, slice.Name, slice.Payload) 125 | case v1.LogSliceType_SLICE_FAIL: 126 | fmt.Printf("[%s%s|FAIL] %s\n", prefix, slice.Name, slice.Payload) 127 | case v1.LogSliceType_SLICE_RESULT: 128 | fmt.Printf("[%s|RESULT] %s\n", slice.Name, slice.Payload) 129 | } 130 | } 131 | 132 | func init() { 133 | jobCmd.AddCommand(jobLogsCmd) 134 | } 135 | -------------------------------------------------------------------------------- /cmd/client/job-stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | v1 "github.com/csweichel/werft/pkg/api/v1" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | // jobStopCmd represents the list command 29 | var jobStopCmd = &cobra.Command{ 30 | Use: "stop [name]", 31 | Short: "Stop a job", 32 | Args: cobra.MaximumNArgs(1), 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | conn := dial() 35 | defer conn.Close() 36 | client := v1.NewWerftServiceClient(conn) 37 | 38 | name, localJobContext, err := getLocalJobName(client, args) 39 | if err != nil { 40 | return err 41 | } 42 | ctx, cancel, err := getRequestContext(localJobContext) 43 | if err != nil { 44 | return err 45 | } 46 | defer cancel() 47 | 48 | _, err = client.StopJob(ctx, &v1.StopJobRequest{ 49 | Name: name, 50 | }) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }, 57 | } 58 | 59 | func init() { 60 | jobCmd.AddCommand(jobStopCmd) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/client/job.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "context" 25 | "os" 26 | 27 | v1 "github.com/csweichel/werft/pkg/api/v1" 28 | "github.com/csweichel/werft/pkg/prettyprint" 29 | "github.com/gogo/protobuf/proto" 30 | log "github.com/sirupsen/logrus" 31 | "github.com/spf13/cobra" 32 | "golang.org/x/xerrors" 33 | ) 34 | 35 | var ( 36 | outputFormat string 37 | outputTemplate string 38 | ) 39 | 40 | // jobCmd represents the job command 41 | var jobCmd = &cobra.Command{ 42 | Use: "job", 43 | Short: "Interacts with currently running or previously run jobs", 44 | Args: cobra.ExactArgs(1), 45 | } 46 | 47 | func init() { 48 | rootCmd.AddCommand(jobCmd) 49 | 50 | jobCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "o", "template", "selects the output format: string, json, yaml, template") 51 | jobCmd.PersistentFlags().StringVar(&outputTemplate, "output-template", "", "template to use in combination with --output-format template") 52 | } 53 | 54 | func prettyPrint(obj proto.Message, defaultTpl string) error { 55 | format := prettyprint.Format(outputFormat) 56 | if !prettyprint.HasFormat(format) { 57 | return xerrors.Errorf("format %s is not supported", format) 58 | } 59 | 60 | tpl := outputTemplate 61 | if tpl == "" { 62 | tpl = defaultTpl 63 | } 64 | 65 | ctnt := &prettyprint.Content{ 66 | Obj: obj, 67 | Format: format, 68 | Writer: os.Stdout, 69 | Template: tpl, 70 | } 71 | return ctnt.Print() 72 | } 73 | 74 | func getMetadataFilter(md *v1.JobMetadata) ([]*v1.FilterExpression, error) { 75 | if md == nil { 76 | return nil, nil 77 | } 78 | return []*v1.FilterExpression{ 79 | {Terms: []*v1.FilterTerm{{Field: "repo.owner", Value: md.Repository.Owner}}}, 80 | {Terms: []*v1.FilterTerm{{Field: "repo.repo", Value: md.Repository.Repo}}}, 81 | {Terms: []*v1.FilterTerm{{Field: "repo.ref", Value: md.Repository.Ref}}}, 82 | }, nil 83 | } 84 | 85 | func findJobByMetadata(ctx context.Context, md *v1.JobMetadata, client v1.WerftServiceClient) (name string, err error) { 86 | filter, err := getMetadataFilter(md) 87 | if err != nil { 88 | return "", err 89 | } 90 | log.WithField("filter", filter).Debug("listing jobs") 91 | 92 | resp, err := client.ListJobs(ctx, &v1.ListJobsRequest{ 93 | Filter: filter, 94 | Order: []*v1.OrderExpression{{ 95 | Field: "created", 96 | Ascending: false, 97 | }}, 98 | }) 99 | if err != nil { 100 | return "", err 101 | } 102 | if len(resp.Result) == 0 { 103 | return "", nil 104 | } 105 | 106 | name = resp.Result[0].Name 107 | return 108 | } 109 | -------------------------------------------------------------------------------- /cmd/client/log-phase.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var logPhaseCmd = &cobra.Command{ 31 | Use: "phase [desc]", 32 | Short: "logs a job result", 33 | Args: cobra.MinimumNArgs(1), 34 | Run: func(cmd *cobra.Command, args []string) { 35 | var ( 36 | phase string 37 | desc string 38 | ) 39 | phase = args[0] 40 | if len(args) > 1 { 41 | desc = strings.Join(args[1:], " ") 42 | } 43 | 44 | fmt.Printf("[%s|PHASE] %s\n", phase, desc) 45 | }, 46 | } 47 | 48 | func init() { 49 | logCmd.AddCommand(logPhaseCmd) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/client/log-result.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var logResultCmd = &cobra.Command{ 31 | Use: "result ", 32 | Short: "logs a job result", 33 | Args: cobra.ExactArgs(2), 34 | Run: func(cmd *cobra.Command, args []string) { 35 | tpe, payload := args[0], args[1] 36 | desc, _ := cmd.Flags().GetString("description") 37 | channels, _ := cmd.Flags().GetStringArray("channels") 38 | 39 | if desc != "" || len(channels) > 0 { 40 | var body struct { 41 | P string `json:"payload"` 42 | C []string `json:"channels,omitempty"` 43 | D string `json:"description,omitempty"` 44 | } 45 | body.P = payload 46 | body.C = channels 47 | body.D = desc 48 | 49 | msg, _ := json.Marshal(body) 50 | fmt.Printf("[%s|RESULT] %s\n", tpe, string(msg)) 51 | return 52 | } 53 | 54 | fmt.Printf("[%s|RESULT] %s\n", tpe, payload) 55 | }, 56 | } 57 | 58 | func init() { 59 | logCmd.AddCommand(logResultCmd) 60 | 61 | logResultCmd.Flags().StringP("description", "d", "", "result description") 62 | logResultCmd.Flags().StringArrayP("channels", "c", []string{}, "result channels (e.g. github or slack)") 63 | } 64 | -------------------------------------------------------------------------------- /cmd/client/log-slice.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | "os" 27 | 28 | "github.com/segmentio/textio" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var logSliceCmd = &cobra.Command{ 33 | Use: "slice ", 34 | Short: "logs a job slice", 35 | Args: cobra.ExactArgs(1), 36 | Run: func(cmd *cobra.Command, args []string) { 37 | name := args[0] 38 | 39 | if fail, _ := cmd.Flags().GetString("fail"); fail != "" { 40 | fmt.Printf("[%s|FAIL] %s\n", name, fail) 41 | return 42 | } 43 | if done, _ := cmd.Flags().GetBool("done"); done { 44 | fmt.Printf("[%s|DONE]\n", name) 45 | return 46 | } 47 | 48 | pw := textio.NewPrefixWriter(os.Stdout, fmt.Sprintf("[%s] ", name)) 49 | defer pw.Flush() 50 | 51 | io.Copy(pw, os.Stdin) 52 | }, 53 | } 54 | 55 | func init() { 56 | logCmd.AddCommand(logSliceCmd) 57 | logSliceCmd.Flags().String("fail", "", "fails the slice") 58 | logSliceCmd.Flags().Bool("done", false, "marks the slice done") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/client/log.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // logCmd represents the job command 28 | var logCmd = &cobra.Command{ 29 | Use: "log", 30 | Short: "Prints log-cuttable content", 31 | Args: cobra.ExactArgs(1), 32 | } 33 | 34 | func init() { 35 | rootCmd.AddCommand(logCmd) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/client/run-previous.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "fmt" 25 | 26 | v1 "github.com/csweichel/werft/pkg/api/v1" 27 | "github.com/golang/protobuf/ptypes" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | // runPreviousJobCmd represents the triggerRemote command 32 | var runPreviousJobCmd = &cobra.Command{ 33 | Use: "previous [old-job-name]", 34 | Short: "starts a job from a previous one", 35 | Args: cobra.MaximumNArgs(1), 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | flags := cmd.Parent().PersistentFlags() 38 | 39 | annotations, _ := flags.GetStringToString("annotations") 40 | if len(annotations) > 0 { 41 | return fmt.Errorf("--annotation is not supported when replaying a previous job") 42 | } 43 | 44 | conn := dial() 45 | defer conn.Close() 46 | client := v1.NewWerftServiceClient(conn) 47 | 48 | name, localJobContext, err := getLocalJobName(client, args) 49 | if err != nil { 50 | return err 51 | } 52 | ctx, cancel, err := getRequestContext(localJobContext) 53 | if err != nil { 54 | return err 55 | } 56 | defer cancel() 57 | 58 | token, _ := cmd.Flags().GetString("token") 59 | req := &v1.StartFromPreviousJobRequest{ 60 | PreviousJob: name, 61 | GithubToken: token, 62 | } 63 | 64 | waitUntil, err := getWaitUntil() 65 | if err != nil { 66 | return err 67 | } 68 | if waitUntil != nil { 69 | req.WaitUntil, err = ptypes.TimestampProto(*waitUntil) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | 75 | resp, err := client.StartFromPreviousJob(ctx, req) 76 | if err != nil { 77 | return err 78 | } 79 | fmt.Println(resp.Status.Name) 80 | 81 | follow, _ := flags.GetBool("follow") 82 | withPrefix, _ := flags.GetString("follow-with-prefix") 83 | if follow || withPrefix != "" { 84 | err = followJob(ctx, client, resp.Status.Name, withPrefix) 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | 90 | return nil 91 | }, 92 | } 93 | 94 | func init() { 95 | runCmd.AddCommand(runPreviousJobCmd) 96 | 97 | runPreviousJobCmd.Flags().String("token", "", "Token to use for authorization against GitHub") 98 | } 99 | -------------------------------------------------------------------------------- /cmd/client/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import ( 22 | "github.com/csweichel/werft/pkg/version" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | // versionCmd represents the version command 27 | var versionCmd = &cobra.Command{ 28 | Use: "version", 29 | Short: "Prints the version of this binary", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | version.Print() 32 | }, 33 | } 34 | 35 | func init() { 36 | rootCmd.AddCommand(versionCmd) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/server/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Copyright © 2019 Christian Weichel 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | 27 | log "github.com/sirupsen/logrus" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | var verbose bool 32 | 33 | // rootCmd represents the base command when called without any subcommands 34 | var rootCmd = &cobra.Command{ 35 | Use: "werft", 36 | Short: "werft is a very simple GitHub triggered and Kubernetes powered CI system", 37 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 38 | if verbose { 39 | log.SetLevel(log.DebugLevel) 40 | log.Debug("verbose logging enabled") 41 | } 42 | }, 43 | 44 | // Uncomment the following line if your bare application 45 | // has an action associated with it: 46 | // Run: func(cmd *cobra.Command, args []string) { }, 47 | } 48 | 49 | // Execute adds all child commands to the root command and sets flags appropriately. 50 | // This is called by main.main(). It only needs to happen once to the rootCmd. 51 | func Execute() { 52 | if err := rootCmd.Execute(); err != nil { 53 | fmt.Println(err) 54 | os.Exit(1) 55 | } 56 | } 57 | 58 | func init() { 59 | rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "en/disable verbose logging") 60 | } 61 | -------------------------------------------------------------------------------- /cmd/server/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import ( 22 | "github.com/csweichel/werft/pkg/version" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | // versionCmd represents the version command 27 | var versionCmd = &cobra.Command{ 28 | Use: "version", 29 | Short: "Prints the version of this binary", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | version.Print() 32 | }, 33 | } 34 | 35 | func init() { 36 | rootCmd.AddCommand(versionCmd) 37 | } 38 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "0.0.4" 3 | description: installs werft.dev 4 | name: werft 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /helm/charts/postgresql-8.1.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/helm/charts/postgresql-8.1.2.tgz -------------------------------------------------------------------------------- /helm/requirements.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: postgresql 3 | repository: https://kubernetes-charts.storage.googleapis.com 4 | version: 8.1.2 5 | digest: sha256:c35fb4a6366561cd8bf128b1910ac994221dce6c9d3ba486fec7a52b5f9d9d03 6 | generated: 2020-01-02T09:31:13.218943737Z 7 | -------------------------------------------------------------------------------- /helm/requirements.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: postgresql 3 | version: 8.1.2 4 | repository: https://kubernetes-charts.storage.googleapis.com 5 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "werft.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "werft.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "werft.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "werft.fullname" . }}-config 5 | labels: 6 | app.kubernetes.io/name: {{ include "werft.name" . }} 7 | helm.sh/chart: {{ include "werft.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | data: 11 | "config.yaml": | 12 | werft: 13 | baseURL: {{ .Values.config.baseURL }} 14 | workspaceNodePathPrefix: {{ .Values.config.workspaceNodePathPrefix }} 15 | service: 16 | webReadOnly: {{ .Values.config.webReadOnly }} 17 | webPort: 8080 18 | grpcPort: 7777 19 | prometheusPort: 9500 20 | pprofPort: 6060 21 | {{- if .Values.config.jobSpecRepos }} 22 | jobSpecRepos: 23 | {{ toYaml .Values.config.jobSpecRepos | indent 8 }} 24 | {{- end }} 25 | executor: 26 | namespace: {{ .Release.Namespace }} 27 | preperationTimeout: {{ .Values.config.timeouts.perperation | default "10m" }} 28 | totalTimeout: {{ .Values.config.timeouts.total | default "60m" }} 29 | storage: 30 | logsPath: /mnt/logs 31 | jobsConnectionString: {{ .Values.config.db | default (printf "host=%s-postgresql dbname=%s user=%s password=%s connect_timeout=5 sslmode=disable" .Release.Name .Values.postgresql.postgresqlDatabase .Values.postgresql.postgresqlUsername .Values.postgresql.postgresqlPassword) }} 32 | plugins: 33 | {{- if .Values.repositories.github }} 34 | - name: "github-repo" 35 | type: 36 | - repository 37 | config: 38 | privateKeyPath: /mnt/secrets/github-app.pem 39 | appID: {{ .Values.repositories.github.appID }} 40 | installationID: {{ .Values.repositories.github.installationID }} 41 | - name: "github-integration" 42 | type: 43 | - integration 44 | config: 45 | baseURL: {{ .Values.config.baseURL }} 46 | webhookSecret: {{ .Values.repositories.github.webhookSecret }} 47 | privateKeyPath: /mnt/secrets/github-app.pem 48 | appID: {{ .Values.repositories.github.appID }} 49 | installationID: {{ .Values.repositories.github.installationID }} 50 | {{- if .Values.repositories.github.integration }} 51 | {{ toYaml .Values.repositories.github.integration | indent 10 }} 52 | {{- end }} 53 | {{- end }} 54 | {{- if .Values.config.plugins }} 55 | {{ toYaml .Values.config.plugins | indent 6 }} 56 | {{- end }} -------------------------------------------------------------------------------- /helm/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "werft.fullname" . }}-secret 5 | labels: 6 | app.kubernetes.io/name: {{ include "werft.name" . }} 7 | helm.sh/chart: {{ include "werft.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | annotations: 11 | {{- if .Values.repositories.github }} 12 | checksum/checksd-config: {{ .Files.Get .Values.repositories.github.privateKeyPath | sha256sum }} 13 | {{- end }} 14 | data: 15 | {{- if .Values.repositories.github }} 16 | github-app.pem: {{ .Files.Get .Values.repositories.github.privateKeyPath | b64enc }} 17 | {{- end }} -------------------------------------------------------------------------------- /helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "werft.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "werft.name" . }} 7 | helm.sh/chart: {{ include "werft.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.ui.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | {{- if .Values.service.grpc }} 18 | - port: {{ .Values.service.grpc.port }} 19 | targetPort: grpc 20 | protocol: TCP 21 | name: grpc 22 | {{- end }} 23 | selector: 24 | app.kubernetes.io/name: {{ include "werft.name" . }} 25 | app.kubernetes.io/instance: {{ .Release.Name }} 26 | -------------------------------------------------------------------------------- /helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "werft.fullname" . }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "werft.name" . }} 8 | helm.sh/chart: {{ include "werft.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | --- 12 | kind: Role 13 | apiVersion: rbac.authorization.k8s.io/v1beta1 14 | metadata: 15 | name: {{ include "werft.fullname" . }} 16 | labels: 17 | app.kubernetes.io/name: {{ include "werft.name" . }} 18 | helm.sh/chart: {{ include "werft.chart" . }} 19 | app.kubernetes.io/instance: {{ .Release.Name }} 20 | app.kubernetes.io/managed-by: {{ .Release.Service }} 21 | rules: 22 | - apiGroups: [""] 23 | resources: ["pods"] 24 | verbs: ["create","delete","get","list","patch","update","watch"] 25 | - apiGroups: [""] 26 | resources: ["pods/exec"] 27 | verbs: ["create","delete","get","list","patch","update","watch"] 28 | - apiGroups: [""] 29 | resources: ["pods/log"] 30 | verbs: ["get","list","watch"] 31 | - apiGroups: [""] 32 | resources: ["secrets"] 33 | verbs: ["get"] 34 | --- 35 | apiVersion: rbac.authorization.k8s.io/v1beta1 36 | kind: RoleBinding 37 | metadata: 38 | name: {{ include "werft.fullname" . }} 39 | labels: 40 | app.kubernetes.io/name: {{ include "werft.name" . }} 41 | helm.sh/chart: {{ include "werft.chart" . }} 42 | app.kubernetes.io/instance: {{ .Release.Name }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | roleRef: 45 | apiGroup: rbac.authorization.k8s.io 46 | kind: Role 47 | name: {{ include "werft.fullname" . }} 48 | subjects: 49 | - kind: ServiceAccount 50 | name: {{ include "werft.fullname" . }} 51 | {{- end -}} -------------------------------------------------------------------------------- /helm/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: {{ include "werft.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "werft.name" . }} 7 | helm.sh/chart: {{ include "werft.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | spec: 11 | serviceName: werft 12 | replicas: {{ .Values.replicaCount }} 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: {{ include "werft.name" . }} 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | updateStrategy: 18 | type: RollingUpdate 19 | volumeClaimTemplates: 20 | - metadata: 21 | name: logs 22 | spec: 23 | accessModes: [ "ReadWriteOnce" ] 24 | resources: 25 | requests: 26 | storage: 10Gi 27 | template: 28 | metadata: 29 | labels: 30 | app.kubernetes.io/name: {{ include "werft.name" . }} 31 | app.kubernetes.io/instance: {{ .Release.Name }} 32 | spec: 33 | serviceAccountName: {{ include "werft.fullname" . }} 34 | volumes: 35 | - name: secrets 36 | secret: 37 | secretName: {{ include "werft.fullname" . }}-secret 38 | - name: config 39 | configMap: 40 | name: {{ include "werft.fullname" . }}-config 41 | containers: 42 | - name: {{ .Chart.Name }} 43 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 44 | imagePullPolicy: {{ .Values.image.pullPolicy }} 45 | args: [ "run", "--verbose", "/mnt/config/config.yaml" ] 46 | ports: 47 | - name: http 48 | containerPort: 8080 49 | protocol: TCP 50 | - name: grpc 51 | containerPort: 7777 52 | protocol: TCP 53 | livenessProbe: 54 | httpGet: 55 | path: / 56 | port: http 57 | failureThreshold: 5 58 | readinessProbe: 59 | httpGet: 60 | path: / 61 | port: http 62 | initialDelaySeconds: 5 63 | volumeMounts: 64 | - name: config 65 | mountPath: "/mnt/config" 66 | readOnly: true 67 | - name: secrets 68 | mountPath: "/mnt/secrets" 69 | readOnly: true 70 | - name: logs 71 | mountPath: "/mnt/logs" 72 | readOnly: false 73 | resources: 74 | {{ toYaml .Values.resources | indent 12 }} 75 | {{- with .Values.nodeSelector }} 76 | nodeSelector: 77 | {{ toYaml . | indent 8 }} 78 | {{- end }} 79 | {{- with .Values.affinity }} 80 | affinity: 81 | {{ toYaml . | indent 8 }} 82 | {{- end }} 83 | {{- with .Values.tolerations }} 84 | tolerations: 85 | {{ toYaml . | indent 8 }} 86 | {{- end }} 87 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | repositories: 2 | github: 3 | webhookSecret: my-webhook-secret 4 | privateKeyPath: secrets/github-app.pem 5 | appID: 000000 6 | installationID: 0000000 7 | integration: 8 | # This section enables users to start werft jobs by adding PR comments containing "/werft run". 9 | pullRequestComments: 10 | # To disable that feature set enabled to false. 11 | enabled: true 12 | # Werft provides feedback by updating that comment. To disable this feedback set updateComment to false. 13 | updateComment: true 14 | # To restrict this feature to users with write access to the repo, set this field to true 15 | requiresWriteAccess: true 16 | # To restrict this feature to users in particular GitHub organisations, add an entry to the requiresOrg list. 17 | requiresOrg: [] 18 | 19 | config: 20 | baseURL: https://demo.werft.dev 21 | # Werft can run its web-UI readonly, s.t. no one can directly start jobs. 22 | # Set this field to true to enable this mode. 23 | webReadOnly: false 24 | ## By default Werft uses an empty-dir to share the workspace between the init container 25 | ## and actual job containers. If you want to use a HostPath mount instead (e.g. for performance reasons), 26 | ## set the path here. Werft will clean up after a job has finished and remove the workspaces 27 | ## it creates. 28 | # workspaceNodePathPrefix: /mnt/disks/ssd0/builds 29 | timeouts: 30 | preperation: 10m 31 | total: 60m 32 | # plugins: 33 | # - name: "cron" 34 | # type: 35 | # - integration 36 | # config: 37 | # tasks: 38 | # - spec: "30 21 * * *" 39 | # repo: github.com/typefox/gitpod:master 40 | # jobPath: .werft/wipe-devstaging.yaml 41 | 42 | replicaCount: 1 43 | 44 | image: 45 | repository: csweichel/werft 46 | tag: latest 47 | pullPolicy: Always 48 | 49 | nameOverride: "" 50 | fullnameOverride: "" 51 | 52 | service: 53 | type: ClusterIP 54 | ui: 55 | port: 80 56 | grpc: 57 | port: 7777 58 | 59 | resources: 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | requests: 68 | cpu: 100m 69 | memory: 128Mi 70 | 71 | nodeSelector: {} 72 | 73 | tolerations: [] 74 | 75 | affinity: {} 76 | 77 | rbac: 78 | create: true 79 | 80 | postgresql: 81 | enabled: true 82 | postgresqlDatabase: werft 83 | postgresqlUsername: werft 84 | postgresqlPassword: changeme 85 | 86 | vouch: {} -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/logo.png -------------------------------------------------------------------------------- /pkg/api/repoconfig/repoconfig.go: -------------------------------------------------------------------------------- 1 | package repoconfig 2 | 3 | import ( 4 | werftv1 "github.com/csweichel/werft/pkg/api/v1" 5 | "github.com/csweichel/werft/pkg/filterexpr" 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | // C is the struct we expect to find in the repo root which configures how we build things 10 | type C struct { 11 | DefaultJob string `yaml:"defaultJob"` 12 | Rules []*JobStartRule `yaml:"rules"` 13 | } 14 | 15 | // JobStartRule determines if a job will be started 16 | type JobStartRule struct { 17 | Path string `yaml:"path"` 18 | Expr []*werftv1.FilterExpression `yaml:"matchesAll"` 19 | } 20 | 21 | // UnmarshalYAML unmarshals the filter expressions 22 | func (r *JobStartRule) UnmarshalYAML(unmarshal func(interface{}) error) error { 23 | var rawJobStartRule struct { 24 | Path string `yaml:"path"` 25 | Expr []JobStartRuleOr `yaml:"matchesAll"` 26 | } 27 | err := unmarshal(&rawJobStartRule) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | r.Path = rawJobStartRule.Path 33 | for _, expr := range rawJobStartRule.Expr { 34 | terms, err := filterexpr.Parse(expr.Or) 35 | if err != nil { 36 | return err 37 | } 38 | r.Expr = append(r.Expr, &werftv1.FilterExpression{Terms: terms}) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // JobStartRuleOr contains an "OR'ed" list of conditions which have to match for a job to run 45 | type JobStartRuleOr struct { 46 | Or []string `yaml:"or"` 47 | } 48 | 49 | // TemplatePath returns the path to the job template in the repo 50 | func (rc *C) TemplatePath(md *werftv1.JobMetadata) string { 51 | js := &werftv1.JobStatus{Metadata: md} 52 | for _, rule := range rc.Rules { 53 | if filterexpr.MatchesFilter(js, rule.Expr) { 54 | return rule.Path 55 | } 56 | } 57 | 58 | return rc.DefaultJob 59 | } 60 | 61 | // ShouldRun determines based on the repo config if the job should run 62 | func (rc *C) ShouldRun(md *werftv1.JobMetadata) bool { 63 | return rc.TemplatePath(md) != "" 64 | } 65 | 66 | // JobSpec is the format of the files we expect to find when starting jobs 67 | type JobSpec struct { 68 | // Desc describes the purpose of this job spec. 69 | Desc string `yaml:"description,omitempty"` 70 | 71 | // Pod is the actual job spec to start. Prior to deploying this to Kubernetes, we'll run this 72 | // as a Go template. 73 | Pod *corev1.PodSpec `yaml:"pod"` 74 | 75 | // Mutex makes job execution exclusive, with new ones canceling the currently running one. 76 | // For example: job A is running at the moment, and job B is about to start. If A and B share the 77 | // same mutex, B will cancel A. 78 | Mutex string `yaml:"mutex,omitempty"` 79 | 80 | // Args describe annotations which this job expects. This list is only used on the UI when manually 81 | // starting the job. 82 | // This is list is neither exhaustive (i.e. jobs can use annotations not listed here), nor binding 83 | // (i.e. jobs can run even when annotations listed here are not present). What matters for a job to 84 | // run is only if Kubernetes accepts the produced podspec. 85 | Args []ArgSpec `yaml:"args,omitempty"` 86 | 87 | // Sidecars list side car containers of the job, i.e. containers 88 | // for which we don't wait that they end to end the job. 89 | Sidecars []string `yaml:"sidecars,omitempty"` 90 | 91 | // Plugins list plugin-specific information 92 | Plugins map[string]string `yaml:"plugins,omitempty"` 93 | } 94 | 95 | // ArgSpec specifies an argument/annotation for a job. 96 | type ArgSpec struct { 97 | Name string `yaml:"name"` 98 | Req bool `yaml:"required"` 99 | Desc string `yaml:"description"` 100 | } 101 | -------------------------------------------------------------------------------- /pkg/api/v1/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | go get github.com/golang/protobuf/protoc-gen-go@v1.3.5 4 | protoc -I. --go_out=plugins=grpc:. *.proto 5 | mv github.com/csweichel/werft/pkg/api/v1/*.go . 6 | rm -r github.com 7 | 8 | go install github.com/golang/mock/mockgen@v1.6.0 9 | mkdir -p mock 10 | mockgen -package=mock -source werft.pb.go > mock/mock.tmp 11 | mv mock/mock.tmp mock/mock.go -------------------------------------------------------------------------------- /pkg/api/v1/werft-ui.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package v1; 4 | option go_package = "github.com/csweichel/werft/pkg/api/v1"; 5 | import "werft.proto"; 6 | 7 | // WerftUI offers services intended for the webui 8 | service WerftUI { 9 | // ListJobSpecs returns a list of jobs that can be started through the UI. 10 | rpc ListJobSpecs(ListJobSpecsRequest) returns (stream ListJobSpecsResponse) {}; 11 | 12 | // IsReadOnly returns true if the UI is readonly. 13 | rpc IsReadOnly(IsReadOnlyRequest) returns (IsReadOnlyResponse) {}; 14 | } 15 | 16 | message ListJobSpecsRequest{} 17 | 18 | message ListJobSpecsResponse { 19 | Repository repo = 1; 20 | string name = 2; 21 | string path = 3; 22 | string description = 4; 23 | repeated DesiredAnnotation arguments = 5; 24 | map plugins = 6; 25 | } 26 | 27 | // DesiredAnnotation describes an annotation a job should have 28 | message DesiredAnnotation { 29 | string name = 1; 30 | bool required = 2; 31 | string description = 3; 32 | } 33 | 34 | message IsReadOnlyRequest {} 35 | 36 | message IsReadOnlyResponse { 37 | bool readonly = 1; 38 | } 39 | -------------------------------------------------------------------------------- /pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | type AuthenticationProvider interface { 10 | // Authenticate tries to authenticate the token 11 | Authenticate(ctx context.Context, token string) (*AuthResponse, error) 12 | } 13 | 14 | type AuthResponse struct { 15 | Known bool `json:"known"` 16 | Username string `json:"username"` 17 | Metadata map[string]string `json:"metadata,omitempty"` 18 | Emails []string `json:"emails,omitempty"` 19 | Teams []string `json:"teams,omitempty"` 20 | } 21 | 22 | type Interceptor interface { 23 | Unary() grpc.UnaryServerInterceptor 24 | Stream() grpc.StreamServerInterceptor 25 | } 26 | -------------------------------------------------------------------------------- /pkg/auth/opa.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "sync" 8 | 9 | "github.com/open-policy-agent/opa/rego" 10 | "github.com/sirupsen/logrus" 11 | log "github.com/sirupsen/logrus" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/metadata" 15 | "google.golang.org/grpc/status" 16 | ) 17 | 18 | func NewOPAInterceptor(ctx context.Context, authProvider AuthenticationProvider, policy func(*rego.Rego)) (Interceptor, error) { 19 | p, err := rego.New( 20 | rego.Query("res = data.werft.allow"), 21 | policy, 22 | ).PrepareForEval(ctx) 23 | if err != nil { 24 | return nil, fmt.Errorf("cannot compile policy: %w", err) 25 | } 26 | return &opaInterceptor{ 27 | Policy: p, 28 | Auth: authProvider, 29 | }, nil 30 | } 31 | 32 | type opaInterceptor struct { 33 | Policy rego.PreparedEvalQuery 34 | Auth AuthenticationProvider 35 | } 36 | 37 | type policyInput struct { 38 | Method string `json:"method"` 39 | Metadata metadata.MD `json:"metadata"` 40 | Message interface{} `json:"message"` 41 | Auth *AuthResponse `json:"auth,omitempty"` 42 | } 43 | 44 | func (i *opaInterceptor) Unary() grpc.UnaryServerInterceptor { 45 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 46 | auth, err := i.getAuth(ctx) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | md, _ := metadata.FromIncomingContext(ctx) 52 | input := policyInput{ 53 | Method: info.FullMethod, 54 | Metadata: md, 55 | Message: req, 56 | Auth: auth, 57 | } 58 | err = i.eval(ctx, input) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return handler(ctx, req) 64 | } 65 | } 66 | 67 | func (i *opaInterceptor) Stream() grpc.StreamServerInterceptor { 68 | return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 69 | ctx := ss.Context() 70 | 71 | auth, err := i.getAuth(ctx) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | md, _ := metadata.FromIncomingContext(ctx) 77 | 78 | return handler(srv, &interceptingStream{ 79 | ServerStream: ss, 80 | eval: func(msg interface{}) error { 81 | input := policyInput{ 82 | Method: info.FullMethod, 83 | Metadata: md, 84 | Message: msg, 85 | Auth: auth, 86 | } 87 | err = i.eval(ctx, input) 88 | if err != nil { 89 | return err 90 | } 91 | return nil 92 | }, 93 | }) 94 | } 95 | } 96 | 97 | type interceptingStream struct { 98 | grpc.ServerStream 99 | eval func(msg interface{}) error 100 | done bool 101 | mu sync.Mutex 102 | } 103 | 104 | func (s *interceptingStream) RecvMsg(m interface{}) error { 105 | err := s.ServerStream.RecvMsg(m) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | s.mu.Lock() 111 | defer s.mu.Unlock() 112 | 113 | if s.done { 114 | return nil 115 | } 116 | s.done = true 117 | 118 | err = s.eval(m) 119 | if err != nil { 120 | return err 121 | } 122 | return nil 123 | } 124 | 125 | func (i *opaInterceptor) eval(ctx context.Context, input policyInput) error { 126 | result, err := i.Policy.Eval(ctx, rego.EvalInput(input)) 127 | if err != nil { 128 | logrus.WithError(err).Error("cannot evaluate policy") 129 | return status.Error(codes.Internal, "cannot evaluate policy") 130 | } 131 | if len(result) == 0 { 132 | logrus.WithError(err).Error("policy does not define data.werft.allow query") 133 | return status.Error(codes.Internal, "invalid policy") 134 | } 135 | 136 | if _, ok := input.Metadata["x-auth-token"]; ok { 137 | input.Metadata["x-auth-token"] = []string{"some-value"} 138 | } 139 | dmp, _ := json.Marshal(input) 140 | allowed, ok := result[0].Bindings["res"].(bool) 141 | logrus.WithField("input", string(dmp)).WithField("allowed", allowed).Debug("evaluating request") 142 | 143 | if !allowed || !ok { 144 | return status.Error(codes.Unauthenticated, "not allowed") 145 | } 146 | return nil 147 | } 148 | 149 | func (i *opaInterceptor) getAuth(ctx context.Context) (*AuthResponse, error) { 150 | md, ok := metadata.FromIncomingContext(ctx) 151 | if !ok { 152 | return nil, nil 153 | } 154 | tkn := md.Get("x-auth-token") 155 | if len(tkn) == 0 { 156 | return nil, nil 157 | } 158 | 159 | aresp, err := i.Auth.Authenticate(ctx, tkn[0]) 160 | if err != nil { 161 | log.WithError(err).Warn("authentication failure") 162 | return nil, status.Errorf(codes.Unauthenticated, "authentication failed") 163 | } 164 | 165 | return aresp, nil 166 | } 167 | -------------------------------------------------------------------------------- /pkg/executor/labels.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import "strings" 4 | 5 | const ( 6 | // DefaultLabelPrefix is used when no explicit label prefix is set 7 | defaultLabelPrefix = "werft.dev" 8 | ) 9 | 10 | type labelSet struct { 11 | // LabelWerftMarker is the label applied to all jobs and configmaps. This label can be used 12 | // to search for werft job objects in Kubernetes. 13 | LabelWerftMarker string 14 | 15 | // LabelJobName adds the ID of the job to the k8s object 16 | LabelJobName string 17 | 18 | // LabelMutex makes jobs findable via their mutex 19 | LabelMutex string 20 | 21 | // UserDataAnnotationPrefix is prepended together with the label prefix to all user annotations added to jobs 22 | UserDataAnnotationPrefix string 23 | 24 | // AnnotationFailureLimit is the annotation denoting the max times a job may fail 25 | AnnotationFailureLimit string 26 | 27 | // AnnotationMetadata stores the JSON encoded metadata available at creation 28 | AnnotationMetadata string 29 | 30 | // AnnotationFailed explicitelly fails the job 31 | AnnotationFailed string 32 | 33 | // AnnotationResults stores JSON encoded list of a job results 34 | AnnotationResults string 35 | 36 | // AnnotationCanReplay stores if this job can be replayed 37 | AnnotationCanReplay string 38 | 39 | // AnnotationWaitUntil stores the start time of waiting job 40 | AnnotationWaitUntil string 41 | 42 | // AnnotationSidecars lists all container whose lifecycle depends on that of the others 43 | AnnotationSidecars string 44 | } 45 | 46 | // newLabelSetet returns a new label set initialized with a particular prefix 47 | func newLabelSetet(prefix string) labelSet { 48 | if prefix == "" { 49 | prefix = defaultLabelPrefix 50 | } 51 | prefix = strings.TrimSuffix(prefix, "/") + "/" 52 | 53 | return labelSet{ 54 | LabelWerftMarker: prefix + "job", 55 | LabelJobName: prefix + "jobName", 56 | LabelMutex: prefix + "mutex", 57 | UserDataAnnotationPrefix: "userdata." + prefix, 58 | AnnotationFailureLimit: prefix + "failureLimit", 59 | AnnotationMetadata: prefix + "metadata", 60 | AnnotationFailed: prefix + "failed", 61 | AnnotationResults: prefix + "results", 62 | AnnotationCanReplay: prefix + "canReplay", 63 | AnnotationWaitUntil: prefix + "waitUntil", 64 | AnnotationSidecars: prefix + "sidecars", 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/executor/loglistener.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/watch" 16 | "k8s.io/client-go/kubernetes" 17 | ) 18 | 19 | type logListener struct { 20 | Clientset kubernetes.Interface 21 | Job string 22 | Namespace string 23 | Labels labelSet 24 | 25 | listener map[string]io.Closer 26 | started time.Time 27 | closed bool 28 | mu sync.RWMutex 29 | 30 | out io.Reader 31 | in io.WriteCloser 32 | inmu sync.Mutex 33 | } 34 | 35 | // Listen establishes a log listener for a job 36 | func listenToLogs(client kubernetes.Interface, job, namespace string, labels labelSet) io.Reader { 37 | ll := &logListener{ 38 | Clientset: client, 39 | Job: job, 40 | Namespace: namespace, 41 | Labels: labels, 42 | started: time.Now(), 43 | listener: make(map[string]io.Closer), 44 | } 45 | ll.out, ll.in = io.Pipe() 46 | go ll.Start() 47 | 48 | return ll.out 49 | } 50 | 51 | func (ll *logListener) Close() error { 52 | ll.mu.Lock() 53 | defer ll.mu.Unlock() 54 | 55 | if ll.closed { 56 | return nil 57 | } 58 | 59 | for id, stp := range ll.listener { 60 | stp.Close() 61 | delete(ll.listener, id) 62 | } 63 | 64 | ll.closed = true 65 | ll.inmu.Lock() 66 | defer ll.inmu.Unlock() 67 | 68 | return ll.in.Close() 69 | } 70 | 71 | func (ll *logListener) Start() { 72 | podwatch, err := ll.Clientset.CoreV1().Pods(ll.Namespace).Watch(context.Background(), metav1.ListOptions{ 73 | LabelSelector: fmt.Sprintf("%s=%s", ll.Labels.LabelJobName, ll.Job), 74 | }) 75 | if err != nil { 76 | log.WithError(err).Warn("cannot watch for pod events") 77 | ll.Close() 78 | return 79 | } 80 | defer podwatch.Stop() 81 | 82 | for { 83 | e := <-podwatch.ResultChan() 84 | if e.Object == nil { 85 | // Closed because of error 86 | return 87 | } 88 | pod, ok := e.Object.(*corev1.Pod) 89 | if !ok { 90 | // not a pod 91 | return 92 | } 93 | 94 | switch e.Type { 95 | case watch.Added, watch.Modified: 96 | var statuses []corev1.ContainerStatus 97 | statuses = append(statuses, pod.Status.InitContainerStatuses...) 98 | statuses = append(statuses, pod.Status.ContainerStatuses...) 99 | 100 | for _, c := range statuses { 101 | if c.State.Running != nil { 102 | var prefix string 103 | if isSidecar := strings.Contains(pod.Annotations[ll.Labels.AnnotationSidecars], c.Name); isSidecar { 104 | prefix = fmt.Sprintf("[%s] ", c.Name) 105 | } 106 | go ll.tail(pod.Name, c.Name, prefix) 107 | } 108 | } 109 | case watch.Deleted: 110 | var statuses []corev1.ContainerStatus 111 | statuses = append(statuses, pod.Status.InitContainerStatuses...) 112 | statuses = append(statuses, pod.Status.ContainerStatuses...) 113 | 114 | for _, c := range statuses { 115 | if c.State.Terminated != nil { 116 | go ll.stopTailing(pod.Name, c.Name) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | func (ll *logListener) tail(pod, container, prefix string) { 124 | var once sync.Once 125 | 126 | ll.mu.Lock() 127 | defer once.Do(ll.mu.Unlock) 128 | 129 | id := fmt.Sprintf("%s/%s", pod, container) 130 | _, ok := ll.listener[id] 131 | if ok { 132 | // we're already listening 133 | return 134 | } 135 | 136 | log.WithField("id", id).Debug("tailing container") 137 | 138 | // we have to start listenting 139 | req := ll.Clientset.CoreV1().Pods(ll.Namespace).GetLogs(pod, &corev1.PodLogOptions{ 140 | Container: container, 141 | Follow: true, 142 | Previous: false, 143 | }) 144 | logs, err := req.Stream(context.Background()) 145 | if err != nil { 146 | log.WithError(err).Debug("cannot connect to logs") 147 | return 148 | } 149 | ll.listener[id] = logs 150 | once.Do(ll.mu.Unlock) 151 | 152 | // forward the logs line by line to ensure we don't mix the output of different conainer 153 | scanner := bufio.NewScanner(logs) 154 | for scanner.Scan() { 155 | line := scanner.Text() 156 | ll.inmu.Lock() 157 | ll.in.Write([]byte(prefix + line + "\n")) 158 | ll.inmu.Unlock() 159 | } 160 | } 161 | 162 | func (ll *logListener) stopTailing(pod, container string) { 163 | ll.mu.Lock() 164 | defer ll.mu.Unlock() 165 | 166 | id := fmt.Sprintf("%s/%s", pod, container) 167 | stp, ok := ll.listener[id] 168 | if !ok { 169 | // we're not listening 170 | return 171 | } 172 | 173 | log.WithField("id", id).Debug("stopped tailing container") 174 | 175 | stp.Close() 176 | delete(ll.listener, id) 177 | } 178 | -------------------------------------------------------------------------------- /pkg/filterexpr/filterexpr.go: -------------------------------------------------------------------------------- 1 | package filterexpr 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | v1 "github.com/csweichel/werft/pkg/api/v1" 8 | "golang.org/x/xerrors" 9 | ) 10 | 11 | // ErrMissingOp indicates that the expression was not complete 12 | var ErrMissingOp = fmt.Errorf("missing operator") 13 | 14 | // Parse parses a list of expressions 15 | func Parse(exprs []string) ([]*v1.FilterTerm, error) { 16 | ops := map[string]v1.FilterOp{ 17 | "==": v1.FilterOp_OP_EQUALS, 18 | "~=": v1.FilterOp_OP_CONTAINS, 19 | "|=": v1.FilterOp_OP_STARTS_WITH, 20 | "=|": v1.FilterOp_OP_ENDS_WITH, 21 | } 22 | 23 | res := make([]*v1.FilterTerm, len(exprs)) 24 | for i, expr := range exprs { 25 | var ( 26 | op v1.FilterOp 27 | opn string 28 | neg bool 29 | ) 30 | for k, v := range ops { 31 | if strings.Contains(expr, "!"+k) { 32 | op = v 33 | opn = "!" + k 34 | neg = true 35 | break 36 | } 37 | if strings.Contains(expr, k) { 38 | op = v 39 | opn = k 40 | break 41 | } 42 | } 43 | if opn == "" { 44 | return nil, ErrMissingOp 45 | } 46 | 47 | segs := strings.Split(expr, opn) 48 | field, val := strings.TrimSpace(segs[0]), strings.TrimSpace(segs[1]) 49 | if field == "success" { 50 | if val == "true" { 51 | val = "1" 52 | } else { 53 | val = "0" 54 | } 55 | } 56 | if field == "phase" { 57 | phn := strings.ToUpper(fmt.Sprintf("PHASE_%s", val)) 58 | if _, ok := v1.JobPhase_value[phn]; !ok { 59 | return nil, xerrors.Errorf("invalid phase: %s", val) 60 | } 61 | } 62 | 63 | res[i] = &v1.FilterTerm{ 64 | Field: field, 65 | Value: val, 66 | Operation: op, 67 | Negate: neg, 68 | } 69 | } 70 | 71 | return res, nil 72 | } 73 | 74 | // MatchesFilter returns true if the annotations are matched by the filter 75 | func MatchesFilter(js *v1.JobStatus, filter []*v1.FilterExpression) (matches bool) { 76 | if len(filter) == 0 { 77 | return true 78 | } 79 | if js == nil { 80 | return false 81 | } 82 | 83 | idx := map[string]string{ 84 | "name": js.Name, 85 | "phase": strings.ToLower(strings.TrimPrefix(js.Phase.String(), "PHASE_")), 86 | } 87 | if js.Metadata != nil { 88 | idx["owner"] = js.Metadata.Owner 89 | idx["trigger"] = strings.ToLower(strings.TrimPrefix(js.Metadata.Trigger.String(), "TRIGGER_")) 90 | if js.Metadata.Repository != nil { 91 | idx["repo.owner"] = js.Metadata.Repository.Owner 92 | idx["repo.repo"] = js.Metadata.Repository.Repo 93 | idx["repo.host"] = js.Metadata.Repository.Host 94 | idx["repo.ref"] = js.Metadata.Repository.Ref 95 | idx["repo.rev"] = js.Metadata.Repository.Revision 96 | } 97 | } 98 | for _, at := range js.Metadata.Annotations { 99 | idx["annotation."+at.Key] = at.Value 100 | } 101 | 102 | matches = true 103 | for _, req := range filter { 104 | var tm bool 105 | for _, alt := range req.Terms { 106 | val, ok := idx[alt.Field] 107 | if !ok { 108 | continue 109 | } 110 | 111 | switch alt.Operation { 112 | case v1.FilterOp_OP_CONTAINS: 113 | tm = strings.Contains(val, alt.Value) 114 | case v1.FilterOp_OP_ENDS_WITH: 115 | tm = strings.HasSuffix(val, alt.Value) 116 | case v1.FilterOp_OP_EQUALS: 117 | tm = val == alt.Value 118 | case v1.FilterOp_OP_STARTS_WITH: 119 | tm = strings.HasPrefix(val, alt.Value) 120 | case v1.FilterOp_OP_EXISTS: 121 | tm = true 122 | } 123 | 124 | if alt.Negate { 125 | tm = !tm 126 | } 127 | 128 | if tm { 129 | break 130 | } 131 | } 132 | 133 | if !tm { 134 | matches = false 135 | break 136 | } 137 | } 138 | return matches 139 | } 140 | -------------------------------------------------------------------------------- /pkg/filterexpr/filterexpr_test.go: -------------------------------------------------------------------------------- 1 | package filterexpr_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/alecthomas/repr" 8 | v1 "github.com/csweichel/werft/pkg/api/v1" 9 | "github.com/csweichel/werft/pkg/filterexpr" 10 | ) 11 | 12 | func TestValidBasics(t *testing.T) { 13 | tests := []struct { 14 | Input string 15 | Result *v1.FilterTerm 16 | Error string 17 | }{ 18 | {"foo==bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_EQUALS, Negate: false}, ""}, 19 | {"foo!==bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_EQUALS, Negate: true}, ""}, 20 | {"foo~=bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_CONTAINS, Negate: false}, ""}, 21 | {"foo!~=bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_CONTAINS, Negate: true}, ""}, 22 | {"foo|=bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_STARTS_WITH, Negate: false}, ""}, 23 | {"foo!|=bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_STARTS_WITH, Negate: true}, ""}, 24 | {"foo=|bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_ENDS_WITH, Negate: false}, ""}, 25 | {"foo!=|bar", &v1.FilterTerm{Field: "foo", Value: "bar", Operation: v1.FilterOp_OP_ENDS_WITH, Negate: true}, ""}, 26 | {"success==true", &v1.FilterTerm{Field: "success", Value: "1", Operation: v1.FilterOp_OP_EQUALS, Negate: false}, ""}, 27 | {"success==false", &v1.FilterTerm{Field: "success", Value: "0", Operation: v1.FilterOp_OP_EQUALS, Negate: false}, ""}, 28 | {"success!==true", &v1.FilterTerm{Field: "success", Value: "1", Operation: v1.FilterOp_OP_EQUALS, Negate: true}, ""}, 29 | {"success!==false", &v1.FilterTerm{Field: "success", Value: "0", Operation: v1.FilterOp_OP_EQUALS, Negate: true}, ""}, 30 | {"trim == whitespace", &v1.FilterTerm{Field: "trim", Value: "whitespace", Operation: v1.FilterOp_OP_EQUALS, Negate: false}, ""}, 31 | {"foo", nil, filterexpr.ErrMissingOp.Error()}, 32 | {"phase==blabla", nil, "invalid phase: blabla"}, 33 | } 34 | 35 | for _, test := range tests { 36 | res, err := filterexpr.Parse([]string{test.Input}) 37 | if err != nil { 38 | if err.Error() != test.Error { 39 | t.Errorf("%s: %v != %v", test.Input, err, test.Error) 40 | } 41 | continue 42 | } 43 | 44 | if len(res) != 1 { 45 | t.Errorf("%s: resulted in NOT exactly one expression, but %v", test.Input, repr.String(res)) 46 | continue 47 | } 48 | if !reflect.DeepEqual(res[0], test.Result) { 49 | t.Errorf("%s: expected %s but got %s", test.Input, repr.String(test.Result), repr.String(res[0])) 50 | continue 51 | } 52 | } 53 | } 54 | 55 | func TestMatchesFilter(t *testing.T) { 56 | md := &v1.JobMetadata{ 57 | Owner: "foo", 58 | Repository: &v1.Repository{}, 59 | Trigger: v1.JobTrigger_TRIGGER_DELETED, 60 | } 61 | tests := []struct { 62 | Job *v1.JobStatus 63 | Expr []*v1.FilterExpression 64 | Matches bool 65 | }{ 66 | { 67 | &v1.JobStatus{Metadata: md, Phase: v1.JobPhase_PHASE_DONE}, 68 | []*v1.FilterExpression{{Terms: []*v1.FilterTerm{{Field: "phase", Value: "done", Operation: v1.FilterOp_OP_EQUALS}}}}, 69 | true, 70 | }, 71 | { 72 | &v1.JobStatus{Metadata: md, Name: "foobar.1"}, 73 | []*v1.FilterExpression{{Terms: []*v1.FilterTerm{{Field: "name", Value: "foobar", Operation: v1.FilterOp_OP_STARTS_WITH}}}}, 74 | true, 75 | }, 76 | { 77 | &v1.JobStatus{Metadata: md, Name: "foo"}, 78 | []*v1.FilterExpression{{Terms: []*v1.FilterTerm{{Field: "trigger", Value: "deleted", Operation: v1.FilterOp_OP_EQUALS, Negate: true}}}}, 79 | false, 80 | }, 81 | } 82 | 83 | for idx, test := range tests { 84 | if filterexpr.MatchesFilter(test.Job, test.Expr) != test.Matches { 85 | t.Errorf("test %d failed", idx) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/logcutter/logcutter.go: -------------------------------------------------------------------------------- 1 | package logcutter 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strings" 7 | 8 | v1 "github.com/csweichel/werft/pkg/api/v1" 9 | ) 10 | 11 | // Cutter splits a log stream into slices for more structured display 12 | type Cutter interface { 13 | // Slice reads on the in reader line-by-line. For each line it can produce several events 14 | // on the events channel. Once the reader returns EOF the events and errchan are closed. 15 | // If anything goes wrong while reading a single error is written to errchan, but nothing is closed. 16 | Slice(in io.Reader) (events <-chan *v1.LogSliceEvent, errchan <-chan error) 17 | } 18 | 19 | const ( 20 | // DefaultSlice is the parent slice of all unmarked content 21 | DefaultSlice = "default" 22 | ) 23 | 24 | // NoCutter does not slice the content up at all 25 | var NoCutter Cutter = noCutter{} 26 | 27 | type noCutter struct{} 28 | 29 | // Slice returns all log lines 30 | func (noCutter) Slice(in io.Reader) (events <-chan *v1.LogSliceEvent, errchan <-chan error) { 31 | evts := make(chan *v1.LogSliceEvent) 32 | errc := make(chan error) 33 | events, errchan = evts, errc 34 | 35 | scanner := bufio.NewScanner(in) 36 | go func() { 37 | for scanner.Scan() { 38 | line := scanner.Text() 39 | evts <- &v1.LogSliceEvent{ 40 | Name: DefaultSlice, 41 | Type: v1.LogSliceType_SLICE_CONTENT, 42 | Payload: line + "\n", 43 | } 44 | } 45 | if err := scanner.Err(); err != nil { 46 | errc <- err 47 | } 48 | close(evts) 49 | close(errc) 50 | }() 51 | 52 | return 53 | } 54 | 55 | // DefaultCutter implements the default cutting behaviour 56 | var DefaultCutter Cutter = defaultCutter{} 57 | 58 | type defaultCutter struct{} 59 | 60 | // Slice cuts a log stream into pieces based on a configurable delimiter 61 | func (defaultCutter) Slice(in io.Reader) (events <-chan *v1.LogSliceEvent, errchan <-chan error) { 62 | evts := make(chan *v1.LogSliceEvent) 63 | errc := make(chan error) 64 | events, errchan = evts, errc 65 | 66 | scanner := bufio.NewScanner(in) 67 | phase := DefaultSlice 68 | go func() { 69 | idx := make(map[string]struct{}) 70 | for scanner.Scan() { 71 | line := scanner.Text() 72 | sl := strings.TrimSpace(line) 73 | 74 | var ( 75 | name string 76 | verb string 77 | payload string 78 | ) 79 | 80 | if !(strings.HasPrefix(sl, "[") && strings.Contains(sl, "]")) { 81 | name = phase 82 | payload = line 83 | } else { 84 | start := strings.IndexRune(sl, '[') 85 | end := strings.IndexRune(sl, ']') 86 | name = sl[start+1 : end] 87 | payload = strings.TrimPrefix(sl[end+1:], " ") 88 | 89 | if segs := strings.Split(name, "|"); len(segs) == 2 { 90 | name = segs[0] 91 | verb = segs[1] 92 | } 93 | } 94 | 95 | switch verb { 96 | case "DONE": 97 | delete(idx, name) 98 | evts <- &v1.LogSliceEvent{ 99 | Name: name, 100 | Type: v1.LogSliceType_SLICE_DONE, 101 | } 102 | continue 103 | case "FAIL": 104 | delete(idx, name) 105 | evts <- &v1.LogSliceEvent{ 106 | Name: name, 107 | Payload: payload, 108 | Type: v1.LogSliceType_SLICE_FAIL, 109 | } 110 | continue 111 | case "RESULT": 112 | evts <- &v1.LogSliceEvent{ 113 | Name: name, 114 | Type: v1.LogSliceType_SLICE_RESULT, 115 | Payload: payload, 116 | } 117 | continue 118 | case "PHASE": 119 | evts <- &v1.LogSliceEvent{ 120 | Name: name, 121 | Type: v1.LogSliceType_SLICE_PHASE, 122 | Payload: payload, 123 | } 124 | phase = name 125 | continue 126 | } 127 | 128 | _, exists := idx[name] 129 | if !exists { 130 | idx[name] = struct{}{} 131 | evts <- &v1.LogSliceEvent{ 132 | Name: name, 133 | Type: v1.LogSliceType_SLICE_START, 134 | } 135 | } 136 | evts <- &v1.LogSliceEvent{ 137 | Name: name, 138 | Type: v1.LogSliceType_SLICE_CONTENT, 139 | Payload: string([]byte(payload)), 140 | } 141 | } 142 | if err := scanner.Err(); err != nil { 143 | errc <- err 144 | } 145 | 146 | for name := range idx { 147 | evts <- &v1.LogSliceEvent{ 148 | Name: name, 149 | Type: v1.LogSliceType_SLICE_ABANDONED, 150 | } 151 | } 152 | 153 | close(evts) 154 | close(errc) 155 | }() 156 | 157 | return 158 | } 159 | -------------------------------------------------------------------------------- /pkg/logcutter/logcutter_test.go: -------------------------------------------------------------------------------- 1 | package logcutter_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | v1 "github.com/csweichel/werft/pkg/api/v1" 11 | "github.com/csweichel/werft/pkg/logcutter" 12 | ) 13 | 14 | func TestDefaultCutterSlice(t *testing.T) { 15 | tests := []struct { 16 | Input string 17 | Events []v1.LogSliceEvent 18 | Error error 19 | }{ 20 | { 21 | ` 22 | [foobar] Hello World this is a test 23 | [otherproc] Some other process 24 | [foobar] More output 25 | [foobar|DONE] 26 | [otherproc] Cool beans 27 | `, 28 | []v1.LogSliceEvent{ 29 | v1.LogSliceEvent{Name: "foobar", Type: v1.LogSliceType_SLICE_START}, 30 | v1.LogSliceEvent{Name: "foobar", Type: v1.LogSliceType_SLICE_CONTENT, Payload: "Hello World this is a test"}, 31 | v1.LogSliceEvent{Name: "otherproc", Type: v1.LogSliceType_SLICE_START}, 32 | v1.LogSliceEvent{Name: "otherproc", Type: v1.LogSliceType_SLICE_CONTENT, Payload: "Some other process"}, 33 | v1.LogSliceEvent{Name: "foobar", Type: v1.LogSliceType_SLICE_CONTENT, Payload: "More output"}, 34 | v1.LogSliceEvent{Name: "foobar", Type: v1.LogSliceType_SLICE_DONE}, 35 | v1.LogSliceEvent{Name: "otherproc", Type: v1.LogSliceType_SLICE_CONTENT, Payload: "Cool beans"}, 36 | v1.LogSliceEvent{Name: "otherproc", Type: v1.LogSliceType_SLICE_ABANDONED}, 37 | }, 38 | nil, 39 | }, 40 | { 41 | ` 42 | [build|PHASE] Pushing foobar 43 | [components/foobar:docker] c13a632cd17b: Preparing 44 | `, 45 | []v1.LogSliceEvent{ 46 | v1.LogSliceEvent{Name: "build", Type: v1.LogSliceType_SLICE_PHASE, Payload: "Pushing foobar"}, 47 | v1.LogSliceEvent{Name: "components/foobar:docker", Type: v1.LogSliceType_SLICE_START}, 48 | v1.LogSliceEvent{Name: "components/foobar:docker", Type: v1.LogSliceType_SLICE_CONTENT, Payload: "c13a632cd17b: Preparing"}, 49 | v1.LogSliceEvent{Name: "components/foobar:docker", Type: v1.LogSliceType_SLICE_ABANDONED}, 50 | }, 51 | nil, 52 | }, 53 | } 54 | 55 | for _, test := range tests { 56 | content := strings.TrimSpace(test.Input) 57 | evtchan, errchan := logcutter.DefaultCutter.Slice(bytes.NewReader([]byte(content))) 58 | 59 | var ( 60 | events []v1.LogSliceEvent 61 | err error 62 | ) 63 | recv: 64 | for { 65 | select { 66 | case evt := <-evtchan: 67 | if evt == nil { 68 | break recv 69 | } 70 | 71 | events = append(events, *evt) 72 | case err = <-errchan: 73 | break recv 74 | } 75 | } 76 | 77 | if err != test.Error { 78 | t.Errorf("unexpected error: \"%s\", expected \"%s\"", err, test.Error) 79 | } 80 | if !reflect.DeepEqual(test.Events, events) { 81 | expevt := make([]string, len(test.Events)) 82 | for i, evt := range test.Events { 83 | expevt[i] = fmt.Sprintf("\t[%s] %s: %s", evt.Name, evt.Type.String(), evt.Payload) 84 | } 85 | actevt := make([]string, len(events)) 86 | for i, evt := range events { 87 | actevt[i] = fmt.Sprintf("\t[%s] %s: %s", evt.Name, evt.Type.String(), evt.Payload) 88 | } 89 | 90 | t.Errorf("unexpected events:\n%s\nexpected:\n%s", strings.Join(actevt, "\n"), strings.Join(expevt, "\n")) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/plugin/common/auth-plugin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package authplugin; 4 | option go_package = "common"; 5 | 6 | service AuthenticationPlugin { 7 | rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {}; 8 | } 9 | 10 | message AuthenticateRequest { 11 | string token = 1; 12 | } 13 | 14 | message AuthenticateResponse { 15 | bool known = 1; 16 | string username = 2; 17 | map metadata = 3; 18 | repeated string emails = 4; 19 | repeated string teams = 5; 20 | } 21 | -------------------------------------------------------------------------------- /pkg/plugin/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Type denotes the plugin type 4 | type Type string 5 | 6 | const ( 7 | // TypeIntegration means the plugin can act as integration plugin 8 | TypeIntegration Type = "integration" 9 | 10 | // TypeRepository means the plugin can add support for remote repositories (e.g. GitHub) 11 | TypeRepository Type = "repository" 12 | 13 | // TypeAuthentication means the plugin can add support for authenticating requests (e.g. against GitHub) 14 | TypeAuthentication Type = "auth" 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/plugin/common/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | go get github.com/golang/protobuf/protoc-gen-go@v1.3.5 4 | protoc -I. -I../../ --go_out=plugins=grpc:. *.proto -------------------------------------------------------------------------------- /pkg/plugin/common/repo-plugin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package repoplugin; 4 | option go_package = "common"; 5 | 6 | import "api/v1/werft.proto"; 7 | 8 | 9 | service RepositoryPlugin { 10 | // RepoHost returns the host which this plugins integrates with 11 | rpc RepoHost(RepoHostRequest) returns (RepoHostResponse) {}; 12 | 13 | // Resolve resolves the repo's revision based on its ref(erence) 14 | rpc Resolve(ResolveRequest) returns (ResolveResponse) {}; 15 | 16 | // ContentInitContainer produces the init container YAML required to initialize 17 | // the build context from this repository in /workspace. 18 | rpc ContentInitContainer(ContentInitContainerRequest) returns (ContentInitContainerResponse) {}; 19 | 20 | // Download downloads a file from the repository. 21 | rpc Download(DownloadRequest) returns (DownloadResponse) {}; 22 | 23 | // ListFiles lists all files in a directory. 24 | rpc ListFiles(ListFilesRequest) returns (ListFilesReponse) {}; 25 | 26 | // GetRemoteAnnotations extracts werft annotations form information associated 27 | // with a particular commit, e.g. the commit message, PRs or merge requests. 28 | // Implementors can expect the revision of the repo object to be set. 29 | rpc GetRemoteAnnotations(GetRemoteAnnotationsRequest) returns (GetRemoteAnnotationsResponse) {}; 30 | } 31 | 32 | message RepoHostRequest {} 33 | 34 | message RepoHostResponse { 35 | string host = 1; 36 | } 37 | 38 | message ResolveRequest { 39 | v1.Repository repository = 1; 40 | } 41 | 42 | message ResolveResponse { 43 | v1.Repository repository = 1; 44 | } 45 | 46 | message ContentInitContainerRequest { 47 | v1.Repository repository = 1; 48 | repeated string paths = 2; 49 | } 50 | 51 | message ContentInitContainerResponse { 52 | bytes container = 1; 53 | } 54 | 55 | message DownloadRequest { 56 | v1.Repository repository = 1; 57 | string path = 2; 58 | } 59 | 60 | message DownloadResponse { 61 | bytes content = 1; 62 | } 63 | 64 | message ListFilesRequest { 65 | v1.Repository repository = 1; 66 | string path = 2; 67 | } 68 | 69 | message ListFilesReponse { 70 | repeated string paths = 1; 71 | } 72 | 73 | message GetRemoteAnnotationsRequest { 74 | v1.Repository repository = 1; 75 | } 76 | 77 | message GetRemoteAnnotationsResponse { 78 | map annotations = 1; 79 | } -------------------------------------------------------------------------------- /pkg/prettyprint/json.go: -------------------------------------------------------------------------------- 1 | package prettyprint 2 | 3 | import "github.com/gogo/protobuf/jsonpb" 4 | 5 | // JSONFormat formats everythign as JSON 6 | const JSONFormat Format = "json" 7 | 8 | func formatJSON(pp *Content) error { 9 | enc := &jsonpb.Marshaler{ 10 | EnumsAsInts: false, 11 | Indent: " ", 12 | } 13 | return enc.Marshal(pp.Writer, pp.Obj) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/prettyprint/prettyprint.go: -------------------------------------------------------------------------------- 1 | package prettyprint 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/gogo/protobuf/proto" 8 | ) 9 | 10 | // Format defines the kind of pretty-printing format we want to use 11 | type Format string 12 | 13 | // HasFormat returns true if the format is supported 14 | func HasFormat(fmt Format) bool { 15 | _, ok := formatter[fmt] 16 | return ok 17 | } 18 | 19 | const ( 20 | // StringFormat uses the Go-builtin stringification for printing 21 | StringFormat Format = "string" 22 | ) 23 | 24 | type formatterFunc func(*Content) error 25 | 26 | var formatter = map[Format]formatterFunc{ 27 | StringFormat: formatString, 28 | TemplateFormat: formatTemplate, 29 | JSONFormat: formatJSON, 30 | YAMLFormat: formatYAML, 31 | } 32 | 33 | func formatString(pp *Content) error { 34 | _, err := fmt.Fprintf(pp.Writer, "%s", pp.Obj) 35 | return err 36 | } 37 | 38 | // Content is pretty-printable content 39 | type Content struct { 40 | Obj proto.Message 41 | Format Format 42 | Writer io.Writer 43 | Template string 44 | } 45 | 46 | // Print outputs the content to its writer in the given format 47 | func (pp *Content) Print() error { 48 | formatter, ok := formatter[pp.Format] 49 | if !ok { 50 | return fmt.Errorf("Unknown format: %s", pp.Format) 51 | } 52 | 53 | return formatter(pp) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/prettyprint/template.go: -------------------------------------------------------------------------------- 1 | package prettyprint 2 | 3 | import ( 4 | "text/tabwriter" 5 | "text/template" 6 | "time" 7 | 8 | "github.com/golang/protobuf/ptypes" 9 | tspb "github.com/golang/protobuf/ptypes/timestamp" 10 | ) 11 | 12 | // TemplateFormat uses Go templates and tabwriter for formatting content 13 | const TemplateFormat Format = "template" 14 | 15 | func formatTemplate(pp *Content) error { 16 | tmpl, err := template. 17 | New("prettyprint"). 18 | Funcs(map[string]interface{}{ 19 | "toRFC3339": func(t *tspb.Timestamp) string { 20 | ts, err := ptypes.Timestamp(t) 21 | if err != nil { 22 | return err.Error() 23 | } 24 | return ts.Format(time.RFC3339) 25 | }, 26 | }). 27 | Parse(pp.Template) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | w := tabwriter.NewWriter(pp.Writer, 8, 8, 8, ' ', 0) 33 | if err := tmpl.Execute(w, pp.Obj); err != nil { 34 | return err 35 | } 36 | if err := w.Flush(); err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/prettyprint/yaml.go: -------------------------------------------------------------------------------- 1 | package prettyprint 2 | 3 | import ( 4 | "gopkg.in/yaml.v3" 5 | ) 6 | 7 | // YAMLFormat formats everythign as YAML 8 | const YAMLFormat Format = "yaml" 9 | 10 | func formatYAML(pp *Content) error { 11 | err := yaml.NewEncoder(pp.Writer).Encode(pp.Obj) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/reporef/reporef.go: -------------------------------------------------------------------------------- 1 | package reporef 2 | 3 | import ( 4 | "strings" 5 | 6 | v1 "github.com/csweichel/werft/pkg/api/v1" 7 | "golang.org/x/xerrors" 8 | ) 9 | 10 | // Parse interprets a string pointing to a (GitHub) repository. 11 | // We expect the string to be in the form of: 12 | // (host)/owner/repo(:ref|@sha) 13 | func Parse(spec string) (*v1.Repository, error) { 14 | if strings.Contains(spec, ":") { 15 | segs := strings.Split(spec, ":") 16 | rep, ref := segs[0], segs[1] 17 | repo, err := parseRep(rep) 18 | if err != nil { 19 | return nil, err 20 | } 21 | repo.Ref = ref 22 | return repo, nil 23 | } 24 | if strings.Contains(spec, "@") { 25 | segs := strings.Split(spec, "@") 26 | rep, rev := segs[0], segs[1] 27 | repo, err := parseRep(rep) 28 | if err != nil { 29 | return nil, err 30 | } 31 | repo.Revision = rev 32 | return repo, nil 33 | } 34 | return parseRep(spec) 35 | } 36 | 37 | func parseRep(rep string) (*v1.Repository, error) { 38 | segs := strings.Split(rep, "/") 39 | if len(segs) < 2 || len(segs) > 3 { 40 | return nil, xerrors.Errorf("invalid repository spec") 41 | } 42 | 43 | res := &v1.Repository{} 44 | if len(segs) == 3 { 45 | res.Host = segs[0] 46 | segs = segs[1:] 47 | } 48 | res.Owner = segs[0] 49 | res.Repo = segs[1] 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/store/logfile_test.go: -------------------------------------------------------------------------------- 1 | package store_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/csweichel/werft/pkg/store" 15 | ) 16 | 17 | func TestContinuousWriteReading(t *testing.T) { 18 | base, err := ioutil.TempDir(os.TempDir(), "tcwr") 19 | if err != nil { 20 | t.Errorf("cannot create test folder: %v", err) 21 | } 22 | 23 | s, err := store.NewFileLogStore(base) 24 | if err != nil { 25 | t.Errorf("cannot create test store: %v", err) 26 | } 27 | 28 | w, err := s.Open("foo") 29 | if err != nil { 30 | t.Errorf("cannot place log: %v", err) 31 | } 32 | r, err := s.Read("foo") 33 | if err != nil { 34 | t.Errorf("cannot read log: %v", err) 35 | } 36 | 37 | var msg = `hello world 38 | this is a test 39 | we're just writing stuff 40 | line by line` 41 | 42 | var wg sync.WaitGroup 43 | wg.Add(2) 44 | go func() { 45 | defer wg.Done() 46 | 47 | lines := strings.Split(msg, "\n") 48 | for i, l := range lines { 49 | if i < len(lines)-1 { 50 | l += "\n" 51 | } 52 | 53 | n, err := w.Write([]byte(l)) 54 | if err != nil { 55 | panic(fmt.Errorf("write error: %v", err)) 56 | } 57 | if n != len(l) { 58 | panic(fmt.Errorf("write error: %v", io.ErrShortWrite)) 59 | } 60 | time.Sleep(10 * time.Millisecond) 61 | } 62 | w.Close() 63 | }() 64 | 65 | rbuf := bytes.NewBuffer(nil) 66 | go func() { 67 | defer wg.Done() 68 | 69 | _, err := io.Copy(rbuf, r) 70 | if err != nil { 71 | t.Errorf("cannot read log: %+v", err) 72 | return 73 | } 74 | }() 75 | 76 | go func() { 77 | time.Sleep(5 * time.Second) 78 | panic("timeout") 79 | }() 80 | wg.Wait() 81 | 82 | actual := rbuf.Bytes() 83 | expected := []byte(msg) 84 | if !bytes.Equal(actual, expected) { 85 | for i, c := range actual { 86 | if i >= len(expected) { 87 | t.Errorf("read more than was written at byte %d: %v", i, c) 88 | continue 89 | } 90 | if c != expected[i] { 91 | t.Errorf("read difference at byte %d: %v !== %v", i, c, expected[i]) 92 | } 93 | } 94 | t.Errorf("did not read message back, but: %s", string(actual)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/store/postgres/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: rice 3 | type: generic 4 | srcs: 5 | - "migrations/*" 6 | - "migration.go" 7 | config: 8 | commands: 9 | - ["go", "mod", "init", "github.com/csweichel/werft/pg"] 10 | - ["go", "get", "github.com/GeertJohan/go.rice/rice"] 11 | - ["sh", "-c", "$GOPATH/bin/rice embed-go"] 12 | - ["rm", "-rf", "migrations", "migration.go"] 13 | - ["go", "fmt", "./..."] 14 | -------------------------------------------------------------------------------- /pkg/store/postgres/leeway.go: -------------------------------------------------------------------------------- 1 | //go:generate sh -c "[ -f ../../../_deps/pkg-store-postgres--rice/rice-box.go ] && cp ../../../_deps/pkg-store-postgres--rice/rice-box.go ." 2 | 3 | package postgres 4 | -------------------------------------------------------------------------------- /pkg/store/postgres/migration.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | 7 | rice "github.com/GeertJohan/go.rice" 8 | "github.com/golang-migrate/migrate/v4" 9 | "github.com/golang-migrate/migrate/v4/database/postgres" 10 | "github.com/golang-migrate/migrate/v4/source" 11 | "github.com/golang-migrate/migrate/v4/source/godoc_vfs" 12 | log "github.com/sirupsen/logrus" 13 | "golang.org/x/tools/godoc/vfs/mapfs" 14 | "golang.org/x/xerrors" 15 | ) 16 | 17 | // Migrate ensures that the database has the current schema required for using any of the postgres storage 18 | func Migrate(db *sql.DB) error { 19 | fs, err := getMigrations(db) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | driver, err := postgres.WithInstance(db, &postgres.Config{}) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | mig, err := migrate.NewWithInstance("godoc-vfs", fs, "postgres", driver) 30 | if err != nil { 31 | return err 32 | } 33 | mig.Log = &logrusAdapter{} 34 | 35 | err = mig.Up() 36 | if err != nil && err != migrate.ErrNoChange { 37 | return xerrors.Errorf("error during migration: %w", err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func getMigrations(db *sql.DB) (source.Driver, error) { 44 | box, err := rice.FindBox("migrations") 45 | if err != nil { 46 | return nil, err 47 | } 48 | migs := make(map[string]string) 49 | err = box.Walk("", func(path string, info os.FileInfo, err error) error { 50 | if err != nil { 51 | return err 52 | } 53 | if info.IsDir() { 54 | return nil 55 | } 56 | 57 | migs[path], err = box.String(path) 58 | if err != nil { 59 | return xerrors.Errorf("cannot read from migration box: %w", err) 60 | } 61 | return nil 62 | }) 63 | if err != nil { 64 | return nil, xerrors.Errorf("cannot list migrations: %w", err) 65 | } 66 | fs, err := godoc_vfs.WithInstance(mapfs.New(migs), "") 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return fs, nil 72 | } 73 | 74 | type logrusAdapter struct{} 75 | 76 | func (*logrusAdapter) Printf(format string, args ...interface{}) { 77 | log.WithField("migration", true).Debugf(format, args...) 78 | } 79 | 80 | func (*logrusAdapter) Verbose() bool { 81 | return true 82 | } 83 | -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20191219172119_initial-schema.down.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE job_status; 3 | DROP TABLE annotations; 4 | DROP TABLE number_group; 5 | -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20191219172119_initial-schema.up.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE IF NOT EXISTS job_status ( 3 | id SERIAL PRIMARY KEY, 4 | name varchar(255) NOT NULL UNIQUE, 5 | data text NOT NULL, 6 | owner varchar(255) NULL, 7 | phase VARCHAR(255) NOT NULL, 8 | repo_owner varchar(255) NULL, 9 | repo_repo varchar(255) NULL, 10 | repo_host varchar(255) NULL, 11 | repo_ref varchar(255) NULL, 12 | trigger_src varchar(255) NULL, 13 | success int not null, 14 | created int not null 15 | ); 16 | 17 | CREATE TABLE IF NOT EXISTS annotations ( 18 | job_id INT NOT NULL, 19 | name varchar(255) NOT NULL, 20 | value text NULL, 21 | CONSTRAINT job_annotation UNIQUE(job_id, name) 22 | ); 23 | 24 | CREATE TABLE IF NOT EXISTS number_group ( 25 | name varchar(255) NOT NULL PRIMARY KEY, 26 | val int NOT NULL 27 | ); 28 | -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20191219180328_indices.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX idx_job_status_name; 2 | DROP INDEX idx_job_status_owner; 3 | DROP INDEX idx_job_status_phase; 4 | DROP INDEX idx_job_status_repo_owner; 5 | DROP INDEX idx_job_status_repo_repo; 6 | DROP INDEX idx_job_status_repo_host; 7 | DROP INDEX idx_job_status_repo_ref; 8 | DROP INDEX idx_job_status_trigger_src; 9 | DROP INDEX idx_job_status_success; 10 | DROP INDEX idx_job_status_created; -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20191219180328_indices.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX idx_job_status_name ON job_status(name); 2 | CREATE INDEX idx_job_status_owner ON job_status(owner); 3 | CREATE INDEX idx_job_status_phase ON job_status(phase); 4 | CREATE INDEX idx_job_status_repo_owner ON job_status(repo_owner); 5 | CREATE INDEX idx_job_status_repo_repo ON job_status(repo_repo); 6 | CREATE INDEX idx_job_status_repo_host ON job_status(repo_host); 7 | CREATE INDEX idx_job_status_repo_ref ON job_status(repo_ref); 8 | CREATE INDEX idx_job_status_trigger_src ON job_status(trigger_src); 9 | CREATE INDEX idx_job_status_success ON job_status(success); 10 | CREATE INDEX idx_job_status_created ON job_status(created); -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20191229110336_job-spec.down.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE job_spec; -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20191229110336_job-spec.up.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE IF NOT EXISTS job_spec ( 3 | name varchar(255) NOT NULL PRIMARY KEY, 4 | data bytea NOT NULL 5 | ); -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20200119172146_did_execute.down.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/store/postgres/migrations/20200119172146_did_execute.down.sql -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20200119172146_did_execute.up.sql: -------------------------------------------------------------------------------- 1 | UPDATE job_status SET data=jsonb_set(data::jsonb, '{conditions, didExecute}', 'true') WHERE data::jsonb->'conditions'->'didExecute' IS NULL; -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20220220151605_job-spec-add.down.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE job_spec 3 | DROP COLUMN spec; -------------------------------------------------------------------------------- /pkg/store/postgres/migrations/20220220151605_job-spec-add.up.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE job_spec 3 | ADD COLUMN spec bytea; -------------------------------------------------------------------------------- /pkg/store/postgres/numbergroup.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/csweichel/werft/pkg/store" 7 | ) 8 | 9 | // NumberGroup provides postgres backed number groups 10 | type NumberGroup struct { 11 | DB *sql.DB 12 | } 13 | 14 | // NewNumberGroup creates a new SQL number group store 15 | func NewNumberGroup(db *sql.DB) (*NumberGroup, error) { 16 | return &NumberGroup{DB: db}, nil 17 | } 18 | 19 | // Latest returns the latest number of a particular number group. 20 | func (ngrp *NumberGroup) Latest(group string) (nr int, err error) { 21 | err = ngrp.DB.QueryRow(` 22 | SELECT val 23 | FROM number_group 24 | WHERE name = $1`, 25 | group, 26 | ).Scan(&nr) 27 | if err == sql.ErrNoRows { 28 | return 0, store.ErrNotFound 29 | } 30 | return 31 | } 32 | 33 | // Next returns the next number in the group. 34 | func (ngrp *NumberGroup) Next(group string) (nr int, err error) { 35 | err = ngrp.DB.QueryRow(` 36 | INSERT 37 | INTO number_group (name, val) 38 | VALUES ($1 , 0 ) 39 | ON CONFLICT (name) DO UPDATE 40 | SET val = number_group.val + 1 41 | RETURNING val`, 42 | group, 43 | ).Scan(&nr) 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /pkg/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | v1 "github.com/csweichel/werft/pkg/api/v1" 10 | ) 11 | 12 | var ( 13 | // ErrNotFound is returned by Read if something isn't found 14 | ErrNotFound = fmt.Errorf("not found") 15 | 16 | // ErrAlreadyExists is returned when attempting to place something which already exists 17 | ErrAlreadyExists = fmt.Errorf("exists already") 18 | ) 19 | 20 | // Logs provides access to the logstore 21 | type Logs interface { 22 | // Open places a logfile in this store. 23 | // The caller is expected to close this writer when the task is complete. 24 | // If the logfile is already open we'll return an error. 25 | Open(id string) (io.WriteCloser, error) 26 | 27 | // Write writes to a previously placed logfile. 28 | // If the logfile is unknown, we'll return an error. 29 | Write(id string) (io.Writer, error) 30 | 31 | // Read retrieves a log file from this store. 32 | // Returns ErrNotFound if the log file isn't found. 33 | // Callers are supposed to close the reader once done. 34 | // Reading from logs currently being written is supported. 35 | Read(id string) (io.ReadCloser, error) 36 | 37 | // GarbageCollect removes all logs older than the given duration. 38 | GarbageCollect(olderThan time.Duration) error 39 | } 40 | 41 | // Jobs provides access to past jobs 42 | type Jobs interface { 43 | // Store stores job information in the store. 44 | // Storing a job whose name we already have in store will override the previously 45 | // stored job. 46 | Store(ctx context.Context, job v1.JobStatus) error 47 | 48 | // StoreJobSpec stores job YAML data. 49 | StoreJobSpec(name string, spec v1.JobSpec, data []byte) error 50 | 51 | // Retrieves a particular job bassd on its name. 52 | // If the job is unknown we'll return ErrNotFound. 53 | Get(ctx context.Context, name string) (*v1.JobStatus, error) 54 | 55 | // Get retrieves previously stored job spec data 56 | GetJobSpec(name string) (spec *v1.JobSpec, data []byte, err error) 57 | 58 | // Searches for jobs based on their annotations. If filter is empty no filter is applied. 59 | // If limit is 0, no limit is applied. 60 | Find(ctx context.Context, filter []*v1.FilterExpression, order []*v1.OrderExpression, start, limit int) (slice []v1.JobStatus, total int, err error) 61 | 62 | // GarbageCollect removes all logs older than the given duration. 63 | GarbageCollect(olderThan time.Duration) error 64 | } 65 | 66 | // NumberGroup enables to atomic generation and storage of numbers. 67 | // This is used for build numbering 68 | type NumberGroup interface { 69 | // Latest returns the latest number of a particular number group. 70 | // Returns ErrNotFound if the group does not exist. A zero result is a valid 71 | // number in a group and does not indicate its non-existence. 72 | Latest(group string) (nr int, err error) 73 | 74 | // Next returns the next number in the group. If the group did not exist prior 75 | // to this call it is created. This function is thread-safe and atomic. 76 | Next(group string) (nr int, err error) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | var ( 6 | // Version is the semver release name of this build 7 | Version string 8 | // Commit is the commit hash this build was created from 9 | Commit string 10 | // Date is the time when this build was created 11 | Date string 12 | ) 13 | 14 | // Print writes the version info to stdout 15 | func Print() { 16 | fmt.Printf("Version: %s\n", Version) 17 | fmt.Printf("Commit: %s\n", Commit) 18 | fmt.Printf("Build date: %s\n", Date) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/webui/.env: -------------------------------------------------------------------------------- 1 | EXTEND_ESLINT=true -------------------------------------------------------------------------------- /pkg/webui/.eslintignore: -------------------------------------------------------------------------------- 1 | src/api -------------------------------------------------------------------------------- /pkg/webui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /pkg/webui/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: dist 3 | type: yarn 4 | srcs: 5 | - src/**/* 6 | - public/**/* 7 | - package.json 8 | - yarn.lock 9 | - .eslintignore 10 | - .env 11 | config: 12 | packaging: archive 13 | dontTest: true 14 | commands: 15 | install: ["yarn", "--network-timeout", "1000000"] 16 | build: ["yarn", "build"] 17 | - name: rice 18 | type: generic 19 | deps: 20 | - :dist 21 | config: 22 | commands: 23 | - ["go", "get", "github.com/GeertJohan/go.rice/rice"] 24 | - ["mkdir", "-p", "pkg/build", "pkg/webui"] 25 | - ["mv", "pkg-webui--dist/build", "pkg/webui/build"] 26 | - ["sh", "-c", "echo cGFja2FnZSB3ZWJ1aQoKaW1wb3J0IHJpY2UgImdpdGh1Yi5jb20vR2VlcnRKb2hhbi9nby5yaWNlIgoKZnVuYyBmb28oKSB7CglyaWNlLkZpbmRCb3goIi4uLy4uL3BrZy93ZWJ1aS9idWlsZCIpCn0K | base64 --decode > pkg/build/index.go"] 27 | - ["sh", "-c", "cd pkg/build && $GOPATH/bin/rice embed-go"] 28 | - ["mv", "pkg/build/rice-box.go", "."] 29 | - ["rm", "-rf", "pkg-webui--dist", "pkg"] 30 | - ["go", "mod", "init", "werft"] 31 | - ["go", "fmt", "./..."] 32 | -------------------------------------------------------------------------------- /pkg/webui/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /pkg/webui/generate.go: -------------------------------------------------------------------------------- 1 | //go:generate sh -c "[ -f ../../_deps/pkg-webui--rice/rice-box.go ] && cp ../../_deps/pkg-webui--rice/rice-box.go ." 2 | 3 | package webui 4 | -------------------------------------------------------------------------------- /pkg/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@iconify/icons-mdi": "^1.0.37", 7 | "@iconify/react": "^1.1.1", 8 | "@improbable-eng/grpc-web": "^0.11.0", 9 | "@material-ui/core": "^4.7.2", 10 | "@material-ui/icons": "^4.5.1", 11 | "@types/jest": "24.0.23", 12 | "@types/node": "12.12.14", 13 | "@types/react": "16.9.14", 14 | "@types/react-dom": "16.9.4", 15 | "material-ui-chip-input": "^2.0.0-beta.2", 16 | "moment": "^2.29.2", 17 | "react": "^16.12.0", 18 | "react-dom": "^16.12.0", 19 | "react-moment": "^0.9.7", 20 | "react-router-dom": "^5.1.2", 21 | "react-scripts": "3.2.0", 22 | "react-timeago": "^4.4.0", 23 | "typescript": "^3.7.2" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "echo", 29 | "eject": "react-scripts eject", 30 | "protoc": "protoc --plugin=\"protoc-gen-ts=$(which protoc-gen-ts)\" --js_out=import_style=commonjs,binary:src/api --ts_out=service=grpc-web:src/api -I../api/v1 werft.proto werft-ui.proto" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/react-router-dom": "^5.1.3", 49 | "@types/react-timeago": "^4.1.1", 50 | "ts-protoc-gen": "^0.12.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/webui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /pkg/webui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /pkg/webui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /pkg/webui/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /pkg/webui/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-16x16.png -------------------------------------------------------------------------------- /pkg/webui/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-32x32.png -------------------------------------------------------------------------------- /pkg/webui/public/favicon-failure-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-failure-16x16.png -------------------------------------------------------------------------------- /pkg/webui/public/favicon-failure-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-failure-32x32.png -------------------------------------------------------------------------------- /pkg/webui/public/favicon-failure.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-failure.ico -------------------------------------------------------------------------------- /pkg/webui/public/favicon-success-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-success-16x16.png -------------------------------------------------------------------------------- /pkg/webui/public/favicon-success-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-success-32x32.png -------------------------------------------------------------------------------- /pkg/webui/public/favicon-success.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon-success.ico -------------------------------------------------------------------------------- /pkg/webui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/favicon.ico -------------------------------------------------------------------------------- /pkg/webui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | werft 25 | 30 | 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /pkg/webui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /pkg/webui/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ebebeb", 17 | "background_color": "#ebebeb", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/webui/public/werft-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/pkg/webui/public/werft-small.png -------------------------------------------------------------------------------- /pkg/webui/src/GithubPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { headerStyles } from './components/header'; 3 | import { createStyles, Theme, Typography, Button } from '@material-ui/core'; 4 | import { WithStyles, withStyles } from '@material-ui/styles'; 5 | 6 | 7 | const styles = (theme: Theme) => createStyles({ 8 | main: { 9 | flex: 1, 10 | padding: theme.spacing(6, 4), 11 | background: '#eaeff1', 12 | }, 13 | button: headerStyles(theme).button, 14 | metadataItemLabel: { 15 | fontWeight: "bold", 16 | paddingRight: "0.5em" 17 | }, 18 | infobar: { 19 | paddingBottom: "1em" 20 | } 21 | }); 22 | 23 | export interface GithubPageProps extends WithStyles { 24 | } 25 | 26 | interface GithubPageState { 27 | } 28 | 29 | class GithubPageImpl extends React.Component { 30 | 31 | render() { 32 | const params = new URLSearchParams(window.location.search); 33 | const isInstallation = params.get('setup_action') === "install" 34 | 35 | return 36 |
37 | Welcome. 38 | { isInstallation && 39 | It would appear you have just installed a GitHub app pointing to this werft installation. To make this installation work you'll need to change the configuration of this installation to include the following: 40 |
41 |                     {`
42 | {
43 |     "webhookSecret": "",
44 |     "privateKeyPath": "",
45 |     "appID": ,
46 |     "installationID": ${params.get('installation_id')}
47 | }
48 | `}
49 |                     
50 |
} 51 | 52 |
53 |
54 | } 55 | 56 | } 57 | 58 | export const GithubPage = withStyles(styles)(GithubPageImpl); 59 | -------------------------------------------------------------------------------- /pkg/webui/src/api/werft-ui_pb_service.d.ts: -------------------------------------------------------------------------------- 1 | // package: v1 2 | // file: werft-ui.proto 3 | 4 | import * as werft_ui_pb from "./werft-ui_pb"; 5 | import {grpc} from "@improbable-eng/grpc-web"; 6 | 7 | type WerftUIListJobSpecs = { 8 | readonly methodName: string; 9 | readonly service: typeof WerftUI; 10 | readonly requestStream: false; 11 | readonly responseStream: true; 12 | readonly requestType: typeof werft_ui_pb.ListJobSpecsRequest; 13 | readonly responseType: typeof werft_ui_pb.ListJobSpecsResponse; 14 | }; 15 | 16 | type WerftUIIsReadOnly = { 17 | readonly methodName: string; 18 | readonly service: typeof WerftUI; 19 | readonly requestStream: false; 20 | readonly responseStream: false; 21 | readonly requestType: typeof werft_ui_pb.IsReadOnlyRequest; 22 | readonly responseType: typeof werft_ui_pb.IsReadOnlyResponse; 23 | }; 24 | 25 | export class WerftUI { 26 | static readonly serviceName: string; 27 | static readonly ListJobSpecs: WerftUIListJobSpecs; 28 | static readonly IsReadOnly: WerftUIIsReadOnly; 29 | } 30 | 31 | export type ServiceError = { message: string, code: number; metadata: grpc.Metadata } 32 | export type Status = { details: string, code: number; metadata: grpc.Metadata } 33 | 34 | interface UnaryResponse { 35 | cancel(): void; 36 | } 37 | interface ResponseStream { 38 | cancel(): void; 39 | on(type: 'data', handler: (message: T) => void): ResponseStream; 40 | on(type: 'end', handler: (status?: Status) => void): ResponseStream; 41 | on(type: 'status', handler: (status: Status) => void): ResponseStream; 42 | } 43 | interface RequestStream { 44 | write(message: T): RequestStream; 45 | end(): void; 46 | cancel(): void; 47 | on(type: 'end', handler: (status?: Status) => void): RequestStream; 48 | on(type: 'status', handler: (status: Status) => void): RequestStream; 49 | } 50 | interface BidirectionalStream { 51 | write(message: ReqT): BidirectionalStream; 52 | end(): void; 53 | cancel(): void; 54 | on(type: 'data', handler: (message: ResT) => void): BidirectionalStream; 55 | on(type: 'end', handler: (status?: Status) => void): BidirectionalStream; 56 | on(type: 'status', handler: (status: Status) => void): BidirectionalStream; 57 | } 58 | 59 | export class WerftUIClient { 60 | readonly serviceHost: string; 61 | 62 | constructor(serviceHost: string, options?: grpc.RpcOptions); 63 | listJobSpecs(requestMessage: werft_ui_pb.ListJobSpecsRequest, metadata?: grpc.Metadata): ResponseStream; 64 | isReadOnly( 65 | requestMessage: werft_ui_pb.IsReadOnlyRequest, 66 | metadata: grpc.Metadata, 67 | callback: (error: ServiceError|null, responseMessage: werft_ui_pb.IsReadOnlyResponse|null) => void 68 | ): UnaryResponse; 69 | isReadOnly( 70 | requestMessage: werft_ui_pb.IsReadOnlyRequest, 71 | callback: (error: ServiceError|null, responseMessage: werft_ui_pb.IsReadOnlyResponse|null) => void 72 | ): UnaryResponse; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /pkg/webui/src/api/werft-ui_pb_service.js: -------------------------------------------------------------------------------- 1 | // package: v1 2 | // file: werft-ui.proto 3 | 4 | var werft_ui_pb = require("./werft-ui_pb"); 5 | var grpc = require("@improbable-eng/grpc-web").grpc; 6 | 7 | var WerftUI = (function () { 8 | function WerftUI() {} 9 | WerftUI.serviceName = "v1.WerftUI"; 10 | return WerftUI; 11 | }()); 12 | 13 | WerftUI.ListJobSpecs = { 14 | methodName: "ListJobSpecs", 15 | service: WerftUI, 16 | requestStream: false, 17 | responseStream: true, 18 | requestType: werft_ui_pb.ListJobSpecsRequest, 19 | responseType: werft_ui_pb.ListJobSpecsResponse 20 | }; 21 | 22 | WerftUI.IsReadOnly = { 23 | methodName: "IsReadOnly", 24 | service: WerftUI, 25 | requestStream: false, 26 | responseStream: false, 27 | requestType: werft_ui_pb.IsReadOnlyRequest, 28 | responseType: werft_ui_pb.IsReadOnlyResponse 29 | }; 30 | 31 | exports.WerftUI = WerftUI; 32 | 33 | function WerftUIClient(serviceHost, options) { 34 | this.serviceHost = serviceHost; 35 | this.options = options || {}; 36 | } 37 | 38 | WerftUIClient.prototype.listJobSpecs = function listJobSpecs(requestMessage, metadata) { 39 | var listeners = { 40 | data: [], 41 | end: [], 42 | status: [] 43 | }; 44 | var client = grpc.invoke(WerftUI.ListJobSpecs, { 45 | request: requestMessage, 46 | host: this.serviceHost, 47 | metadata: metadata, 48 | transport: this.options.transport, 49 | debug: this.options.debug, 50 | onMessage: function (responseMessage) { 51 | listeners.data.forEach(function (handler) { 52 | handler(responseMessage); 53 | }); 54 | }, 55 | onEnd: function (status, statusMessage, trailers) { 56 | listeners.status.forEach(function (handler) { 57 | handler({ code: status, details: statusMessage, metadata: trailers }); 58 | }); 59 | listeners.end.forEach(function (handler) { 60 | handler({ code: status, details: statusMessage, metadata: trailers }); 61 | }); 62 | listeners = null; 63 | } 64 | }); 65 | return { 66 | on: function (type, handler) { 67 | listeners[type].push(handler); 68 | return this; 69 | }, 70 | cancel: function () { 71 | listeners = null; 72 | client.close(); 73 | } 74 | }; 75 | }; 76 | 77 | WerftUIClient.prototype.isReadOnly = function isReadOnly(requestMessage, metadata, callback) { 78 | if (arguments.length === 2) { 79 | callback = arguments[1]; 80 | } 81 | var client = grpc.unary(WerftUI.IsReadOnly, { 82 | request: requestMessage, 83 | host: this.serviceHost, 84 | metadata: metadata, 85 | transport: this.options.transport, 86 | debug: this.options.debug, 87 | onEnd: function (response) { 88 | if (callback) { 89 | if (response.status !== grpc.Code.OK) { 90 | var err = new Error(response.statusMessage); 91 | err.code = response.status; 92 | err.metadata = response.trailers; 93 | callback(err, null); 94 | } else { 95 | callback(null, response.message); 96 | } 97 | } 98 | } 99 | }); 100 | return { 101 | cancel: function () { 102 | callback = null; 103 | client.close(); 104 | } 105 | }; 106 | }; 107 | 108 | exports.WerftUIClient = WerftUIClient; 109 | 110 | -------------------------------------------------------------------------------- /pkg/webui/src/components/IsReadonly.tsx: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | import { WerftUIClient } from "../api/werft-ui_pb_service"; 3 | import { IsReadOnlyRequest } from '../api/werft-ui_pb'; 4 | 5 | export interface IsReadonlyProps { 6 | uiClient: WerftUIClient; 7 | } 8 | 9 | export interface IsReadonlyState { 10 | readonly: boolean; 11 | } 12 | 13 | export class IsReadonly extends React.Component { 14 | 15 | constructor(props: IsReadonlyProps) { 16 | super(props); 17 | this.state = { readonly: true }; 18 | } 19 | 20 | componentDidMount() { 21 | try { 22 | this.props.uiClient.isReadOnly(new IsReadOnlyRequest(), (err, msg) => { 23 | if (err) { 24 | console.warn("cannot determine if UI is readonly", err); 25 | return; 26 | } 27 | 28 | this.setState({ readonly: msg!.getReadonly() }); 29 | }); 30 | } catch (err) { 31 | console.warn(err); 32 | } 33 | } 34 | 35 | render() { 36 | return Children.map(this.props.children, c => React.isValidElement(c) ? React.cloneElement(c, { readonly: this.state.readonly }) : undefined); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /pkg/webui/src/components/Naviagor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; 4 | import Divider from '@material-ui/core/Divider'; 5 | import Drawer, { DrawerProps } from '@material-ui/core/Drawer'; 6 | import List from '@material-ui/core/List'; 7 | import ListItem from '@material-ui/core/ListItem'; 8 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 9 | import ListItemText from '@material-ui/core/ListItemText'; 10 | import HomeIcon from '@material-ui/icons/Home'; 11 | import PlayArrowIcon from '@material-ui/icons/PlayArrow'; 12 | import { Omit } from '@material-ui/types'; 13 | 14 | const categories = [ 15 | { 16 | id: 'Control', 17 | children: [ 18 | { id: 'Launch', icon: , active: false }, 19 | ], 20 | }, 21 | ]; 22 | 23 | const styles = (theme: Theme) => 24 | createStyles({ 25 | categoryHeader: { 26 | paddingTop: theme.spacing(2), 27 | paddingBottom: theme.spacing(2), 28 | }, 29 | categoryHeaderPrimary: { 30 | color: theme.palette.common.white, 31 | }, 32 | item: { 33 | paddingTop: 1, 34 | paddingBottom: 1, 35 | color: 'rgba(255, 255, 255, 0.7)', 36 | '&:hover,&:focus': { 37 | backgroundColor: 'rgba(255, 255, 255, 0.08)', 38 | }, 39 | }, 40 | itemCategory: { 41 | backgroundColor: '#232f3e', 42 | boxShadow: '0 -1px 0 #404854 inset', 43 | paddingTop: theme.spacing(2), 44 | paddingBottom: theme.spacing(2), 45 | }, 46 | firebase: { 47 | fontSize: 24, 48 | color: theme.palette.common.white, 49 | }, 50 | itemActiveItem: { 51 | color: '#4fc3f7', 52 | }, 53 | itemPrimary: { 54 | fontSize: 'inherit', 55 | }, 56 | itemIcon: { 57 | minWidth: 'auto', 58 | marginRight: theme.spacing(2), 59 | }, 60 | divider: { 61 | marginTop: theme.spacing(2), 62 | }, 63 | }); 64 | 65 | export interface NavigatorProps extends Omit, WithStyles { } 66 | 67 | 68 | class NavigatorImpl extends React.Component { 69 | 70 | render() { 71 | const { classes, ...other } = this.props; 72 | return ( 73 | 74 | 75 | werft 76 | 77 | 78 | Jobs 79 | 80 | {categories.map(({ id, children }) => ( 81 | 82 | 83 | {id} 84 | 85 | {children.map(({ id: childId, icon, active }) => ( 86 | 91 | {icon} 92 | 97 | {childId} 98 | 99 | 100 | ))} 101 | 102 | 103 | ))} 104 | 105 | 106 | ); 107 | } 108 | } 109 | 110 | export const Navigator = withStyles(styles)(NavigatorImpl); 111 | -------------------------------------------------------------------------------- /pkg/webui/src/components/ResultView.tsx: -------------------------------------------------------------------------------- 1 | import { Theme, createStyles, WithStyles, List, ListItem, ListItemText, Link, ListItemAvatar } from "@material-ui/core"; 2 | import { JobStatus } from '../api/werft_pb'; 3 | import * as React from 'react'; 4 | import { withStyles } from "@material-ui/styles"; 5 | import ReceiptIcon from '@material-ui/icons/ReceiptOutlined'; 6 | import LinkIcon from '@material-ui/icons/Link'; 7 | import { InlineIcon } from "@iconify/react"; 8 | import docckerIcon from "@iconify/icons-mdi/docker"; 9 | 10 | export const styles = (theme: Theme) => 11 | createStyles({ 12 | 13 | }); 14 | 15 | export interface ResultViewProps extends WithStyles { 16 | status?: JobStatus.AsObject 17 | } 18 | 19 | const ResultViewImpl: React.SFC = (props) => { 20 | if (!props.status) { 21 | return ; 22 | } 23 | 24 | const results = props.status.resultsList.slice().reverse(); 25 | return 26 | { results.map((r, i) => ( 27 | 28 | {renderIcon(r.type)} 29 | 30 | 31 | )) } 32 | ; 33 | }; 34 | 35 | function renderIcon(type: string) { 36 | let icon: JSX.Element; 37 | switch (type) { 38 | case "url": icon = ; break; 39 | case "docker": icon = ; break; 40 | default: icon = ; break; 41 | } 42 | return 43 | {icon} 44 | 45 | } 46 | 47 | function renderPayload(type: string, payload: string) { 48 | switch (type) { 49 | case "url": return {payload} 50 | case "docker": return docker pull {payload} 51 | } 52 | } 53 | 54 | export const ResultView = withStyles(styles)(ResultViewImpl); 55 | -------------------------------------------------------------------------------- /pkg/webui/src/components/StickyScroll.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | 4 | interface StickyScrollState { 5 | enabled: boolean 6 | } 7 | 8 | export interface StickyScrollProps { 9 | }; 10 | 11 | export class StickyScroll extends React.Component { 12 | protected endOfLine: HTMLDivElement | null = null; 13 | protected container: HTMLDivElement | null = null; 14 | 15 | constructor(p: StickyScrollProps) { 16 | super(p); 17 | this.state = { 18 | enabled: false 19 | }; 20 | this.onScroll = this.onScroll.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | this.scrollToBottom(); 25 | window.addEventListener('scroll', this.onScroll); 26 | } 27 | 28 | componentWillUnmount() { 29 | window.removeEventListener('scroll', this.onScroll); 30 | } 31 | 32 | componentDidUpdate() { 33 | this.scrollToBottom(); 34 | } 35 | 36 | protected scrollToBottom() { 37 | if (!this.state.enabled) { 38 | return; 39 | } 40 | if (!this.endOfLine) { 41 | return; 42 | } 43 | 44 | this.endOfLine.scrollIntoView({ behavior: "smooth" }); 45 | } 46 | 47 | protected onScroll() { 48 | let stick: boolean; 49 | if (this.state.enabled) { 50 | stick = (window.innerHeight + window.pageYOffset) >= document.body.offsetHeight - 500; 51 | } else { 52 | stick = (window.innerHeight + window.pageYOffset) >= document.body.offsetHeight - 100; 53 | } 54 | 55 | if (this.state.enabled !== stick) { 56 | this.setState({enabled: stick}); 57 | } 58 | } 59 | 60 | render() { 61 | return 62 |
this.container = el}> 63 | {this.props.children}
this.endOfLine = el} /> 64 |
65 | 66 | } 67 | } -------------------------------------------------------------------------------- /pkg/webui/src/components/colors.ts: -------------------------------------------------------------------------------- 1 | 2 | export const ColorSuccess = '#2EC990'; 3 | export const ColorFailure = '#F75E60'; 4 | export const ColorWarning = '#FCB008'; 5 | export const ColorUnknown = '#CCCCCC'; 6 | export const ColorRunning = '#A3B2BD'; -------------------------------------------------------------------------------- /pkg/webui/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from '@material-ui/core/AppBar'; 3 | import Grid from '@material-ui/core/Grid'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles'; 7 | 8 | const lightColor = 'rgba(255, 255, 255, 0.7)'; 9 | 10 | export const headerStyles = (theme: Theme) => 11 | createStyles({ 12 | mainBar: { 13 | zIndex: 999, 14 | }, 15 | secondaryBar: { 16 | paddingTop: '0.5em', 17 | zIndex: 2, 18 | }, 19 | thirdBar: { 20 | zIndex: 1, 21 | }, 22 | menuButton: { 23 | marginLeft: -theme.spacing(1), 24 | }, 25 | iconButtonAvatar: { 26 | padding: 4, 27 | }, 28 | link: { 29 | textDecoration: 'none', 30 | color: lightColor, 31 | '&:hover': { 32 | color: theme.palette.common.white, 33 | }, 34 | }, 35 | button: { 36 | // borderColor: lightColor, 37 | }, 38 | }); 39 | 40 | export interface HeaderProps extends WithStyles { 41 | title: string 42 | color?: string 43 | actions?: React.ReactFragment 44 | secondary?: React.ReactFragment 45 | thirdrow?: React.ReactFragment 46 | } 47 | 48 | interface HeaderState {} 49 | 50 | class HeaderImpl extends React.Component { 51 | 52 | render() { 53 | const { classes } = this.props; 54 | let appbarStyle = {}; 55 | if (this.props.color) { 56 | appbarStyle = { backgroundColor: this.props.color}; 57 | } 58 | 59 | return ( 60 | 61 | 69 | 70 | 71 | 72 | 73 | {this.props.title} 74 | 75 | 76 | {this.props.actions} 77 | 78 | 79 | 80 | { this.props.secondary && 81 | {this.props.secondary} 89 | } 90 | { this.props.thirdrow && 91 | {this.props.thirdrow} 99 | } 100 | 101 | ) 102 | } 103 | } 104 | 105 | export const Header = withStyles(headerStyles)(HeaderImpl); 106 | -------------------------------------------------------------------------------- /pkg/webui/src/components/util.ts: -------------------------------------------------------------------------------- 1 | import { JobPhase, JobPhaseMap } from "../api/werft_pb"; 2 | 3 | export function debounce(f: (a: T) => void, interval: number): (a: T) => void { 4 | let tc: any | undefined; 5 | 6 | return (a: T) => { 7 | if (tc !== undefined) { 8 | clearTimeout(tc); 9 | } 10 | tc = setTimeout(() => f(a), 200); 11 | } 12 | } 13 | 14 | export function phaseToString(p: JobPhaseMap[keyof JobPhaseMap]): string { 15 | const kvs = Object.getOwnPropertyNames(JobPhase).map(k => [k, (JobPhase as any)[k]]).find(kv => kv[1] === p); 16 | return kvs![0].split("_")[1].toLowerCase(); 17 | } -------------------------------------------------------------------------------- /pkg/webui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import { unregister } from './registerServiceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | unregister(); -------------------------------------------------------------------------------- /pkg/webui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /pkg/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": false, 10 | "checkJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /pkg/webui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-react", 5 | "tslint-config-prettier" 6 | ], 7 | "linterOptions": { 8 | "exclude": [ 9 | "config/**/*.js", 10 | "node_modules/**/*.ts", 11 | "coverage/lcov-report/*.js", 12 | "src/api/*.js", 13 | "src/api/*.ts" 14 | ] 15 | }, 16 | "rules": { 17 | "ordered-imports": [ 18 | false 19 | ], 20 | "object-literal-sort-keys": [ 21 | false 22 | ], 23 | "interface-name": [ 24 | false 25 | ], 26 | "no-console": false, 27 | "max-classes-per-file": [true, 5] 28 | } 29 | } -------------------------------------------------------------------------------- /pkg/werft/service_test.go: -------------------------------------------------------------------------------- 1 | package werft 2 | 3 | import "testing" 4 | 5 | func TestCleanupPodName(t *testing.T) { 6 | tests := []struct { 7 | Input string 8 | Expectation string 9 | }{ 10 | {"this-is-an-invalid-podname-.33", "this-is-an-invalid-podnamea.33"}, 11 | {"", "unknown"}, 12 | // This test case happens to be shortened s.t. it ends with a dash, which is invalid. 13 | // The cleanup function should not let that happen. 14 | {"this-is-way-too-long-this-is-way-too-long-this-is-way-too-long", "this-is-way-too-long-this-is-way-too-long-this-is-way-tooa"}, 15 | } 16 | 17 | for _, test := range tests { 18 | t.Run(test.Input, func(t *testing.T) { 19 | act := cleanupPodName(test.Input) 20 | if act != test.Expectation { 21 | t.Errorf("unexpected result: \"%s\"; expected \"%s\"", act, test.Expectation) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugins/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: all 3 | type: generic 4 | deps: 5 | - plugins/cron:app 6 | - plugins/github-auth:app 7 | - plugins/github-repo:app 8 | - plugins/github-integration:app 9 | - plugins/integration-example:app 10 | - plugins/otel-exporter:app 11 | - plugins/webhook:app 12 | config: 13 | commands: 14 | - ["mkdir", "bin"] 15 | - ["sh", "-c", "find . -name \"werft-*\" -exec mv {} bin \\;"] 16 | -------------------------------------------------------------------------------- /plugins/cron/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | srcs: 7 | - "**/*.go" 8 | - "go.mod" 9 | - "go.sum" 10 | env: 11 | - CGO_ENABLED=0 12 | config: 13 | buildFlags: ["-o", "werft-plugin-cron"] -------------------------------------------------------------------------------- /plugins/cron/README.md: -------------------------------------------------------------------------------- 1 | This plugin starts jobs based on time. 2 | For example: 3 | ``` 4 | tasks: 5 | - spec: "@every 10m" 6 | repo: github.com/32leaves/test-repo:werft 7 | ``` 8 | 9 | See https://godoc.org/github.com/robfig/cron for more details about the time specification. 10 | Have a look at the `Config` struct in `main.go` w.r.t the configuration format. -------------------------------------------------------------------------------- /plugins/cron/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/csweichel/werft/plugins/cron 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/csweichel/werft v0.0.0-00010101000000-000000000000 7 | github.com/robfig/cron/v3 v3.0.1 8 | github.com/sirupsen/logrus v1.8.1 9 | ) 10 | 11 | require ( 12 | github.com/golang/protobuf v1.5.2 // indirect 13 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 14 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 15 | golang.org/x/text v0.3.7 // indirect 16 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 17 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 18 | google.golang.org/grpc v1.45.0 // indirect 19 | google.golang.org/protobuf v1.28.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 21 | ) 22 | 23 | replace github.com/csweichel/werft => ../.. // leeway 24 | 25 | replace k8s.io/api => k8s.io/api v0.23.4 // leeway indirect from //:plugin-client-lib 26 | 27 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 28 | 29 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.23.4 // leeway indirect from //:plugin-client-lib 30 | 31 | replace k8s.io/apiserver => k8s.io/apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 32 | 33 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.4 // leeway indirect from //:plugin-client-lib 34 | 35 | replace k8s.io/client-go => k8s.io/client-go v0.23.4 // leeway indirect from //:plugin-client-lib 36 | 37 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.4 // leeway indirect from //:plugin-client-lib 38 | 39 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.4 // leeway indirect from //:plugin-client-lib 40 | 41 | replace k8s.io/code-generator => k8s.io/code-generator v0.23.4 // leeway indirect from //:plugin-client-lib 42 | 43 | replace k8s.io/component-base => k8s.io/component-base v0.23.4 // leeway indirect from //:plugin-client-lib 44 | 45 | replace k8s.io/cri-api => k8s.io/cri-api v0.23.4 // leeway indirect from //:plugin-client-lib 46 | 47 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.4 // leeway indirect from //:plugin-client-lib 48 | 49 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.4 // leeway indirect from //:plugin-client-lib 50 | 51 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 52 | 53 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.4 // leeway indirect from //:plugin-client-lib 54 | 55 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.4 // leeway indirect from //:plugin-client-lib 56 | 57 | replace k8s.io/kubelet => k8s.io/kubelet v0.23.4 // leeway indirect from //:plugin-client-lib 58 | 59 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.4 // leeway indirect from //:plugin-client-lib 60 | 61 | replace k8s.io/metrics => k8s.io/metrics v0.23.4 // leeway indirect from //:plugin-client-lib 62 | 63 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 64 | 65 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.23.4 // leeway indirect from //:plugin-client-lib 66 | 67 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 68 | 69 | replace k8s.io/kubectl => k8s.io/kubectl v0.23.4 // leeway indirect from //:plugin-client-lib 70 | 71 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.23.4 // leeway indirect from //:plugin-client-lib 72 | -------------------------------------------------------------------------------- /plugins/github-auth/.gitignore: -------------------------------------------------------------------------------- 1 | github-repo -------------------------------------------------------------------------------- /plugins/github-auth/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | srcs: 7 | - "**/*.go" 8 | - "go.mod" 9 | - "go.sum" 10 | env: 11 | - CGO_ENABLED=0 12 | config: 13 | buildFlags: ["-o", "werft-plugin-github-auth"] -------------------------------------------------------------------------------- /plugins/github-auth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/csweichel/werft/plugins/github-auth 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/csweichel/werft v0.0.0-00010101000000-000000000000 7 | github.com/google/go-github/v43 v43.0.0 8 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 9 | ) 10 | 11 | require ( 12 | github.com/golang/protobuf v1.5.2 // indirect 13 | github.com/google/go-querystring v1.1.0 // indirect 14 | github.com/kr/text v0.2.0 // indirect 15 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 16 | github.com/sirupsen/logrus v1.8.1 // indirect 17 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 18 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 19 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 20 | golang.org/x/text v0.3.7 // indirect 21 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 22 | google.golang.org/appengine v1.6.7 // indirect 23 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 24 | google.golang.org/grpc v1.45.0 // indirect 25 | google.golang.org/protobuf v1.28.0 // indirect 26 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 27 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 28 | ) 29 | 30 | replace github.com/csweichel/werft => ../.. // leeway 31 | 32 | replace k8s.io/api => k8s.io/api v0.23.4 // leeway indirect from //:plugin-client-lib 33 | 34 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 35 | 36 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.23.4 // leeway indirect from //:plugin-client-lib 37 | 38 | replace k8s.io/apiserver => k8s.io/apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 39 | 40 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.4 // leeway indirect from //:plugin-client-lib 41 | 42 | replace k8s.io/client-go => k8s.io/client-go v0.23.4 // leeway indirect from //:plugin-client-lib 43 | 44 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.4 // leeway indirect from //:plugin-client-lib 45 | 46 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.4 // leeway indirect from //:plugin-client-lib 47 | 48 | replace k8s.io/code-generator => k8s.io/code-generator v0.23.4 // leeway indirect from //:plugin-client-lib 49 | 50 | replace k8s.io/component-base => k8s.io/component-base v0.23.4 // leeway indirect from //:plugin-client-lib 51 | 52 | replace k8s.io/cri-api => k8s.io/cri-api v0.23.4 // leeway indirect from //:plugin-client-lib 53 | 54 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.4 // leeway indirect from //:plugin-client-lib 55 | 56 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.4 // leeway indirect from //:plugin-client-lib 57 | 58 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 59 | 60 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.4 // leeway indirect from //:plugin-client-lib 61 | 62 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.4 // leeway indirect from //:plugin-client-lib 63 | 64 | replace k8s.io/kubelet => k8s.io/kubelet v0.23.4 // leeway indirect from //:plugin-client-lib 65 | 66 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.4 // leeway indirect from //:plugin-client-lib 67 | 68 | replace k8s.io/metrics => k8s.io/metrics v0.23.4 // leeway indirect from //:plugin-client-lib 69 | 70 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 71 | 72 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.23.4 // leeway indirect from //:plugin-client-lib 73 | 74 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 75 | 76 | replace k8s.io/kubectl => k8s.io/kubectl v0.23.4 // leeway indirect from //:plugin-client-lib 77 | 78 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.23.4 // leeway indirect from //:plugin-client-lib 79 | -------------------------------------------------------------------------------- /plugins/github-auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "reflect" 9 | "strconv" 10 | 11 | plugin "github.com/csweichel/werft/pkg/plugin/client" 12 | "github.com/csweichel/werft/pkg/plugin/common" 13 | "golang.org/x/oauth2" 14 | 15 | "github.com/google/go-github/v43/github" 16 | ) 17 | 18 | // Config configures this plugin 19 | type Config struct { 20 | EnableTeams bool `yaml:"enableTeams,omitempty"` 21 | EnableEmails bool `yaml:"enableEmails,omitempty"` 22 | } 23 | 24 | func main() { 25 | plugin.Serve(&Config{}, 26 | plugin.WithAuthenticationPlugin(&githubAuthPlugin{}), 27 | ) 28 | fmt.Fprintln(os.Stderr, "shutting down") 29 | } 30 | 31 | type githubAuthPlugin struct{} 32 | 33 | func (*githubAuthPlugin) Run(ctx context.Context, config interface{}) (common.AuthenticationPluginServer, error) { 34 | cfg, ok := config.(*Config) 35 | if !ok { 36 | return nil, fmt.Errorf("config has wrong type %s", reflect.TypeOf(config)) 37 | } 38 | 39 | return &authServer{Config: cfg}, nil 40 | } 41 | 42 | type authServer struct { 43 | Config *Config 44 | } 45 | 46 | func (as *authServer) Authenticate(ctx context.Context, req *common.AuthenticateRequest) (*common.AuthenticateResponse, error) { 47 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: req.Token}) 48 | tc := oauth2.NewClient(ctx, ts) 49 | client := github.NewClient(tc) 50 | 51 | ghreq, err := client.NewRequest("GET", "user", nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | user := new(github.User) 57 | resp, err := client.Do(ctx, ghreq, user) 58 | if resp.StatusCode == http.StatusUnauthorized { 59 | return &common.AuthenticateResponse{Known: false}, nil 60 | } 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | var emails []string 66 | if as.Config.EnableEmails { 67 | rawEmails, _, _ := client.Users.ListEmails(ctx, nil) 68 | for _, e := range rawEmails { 69 | if !e.GetVerified() { 70 | continue 71 | } 72 | 73 | emails = append(emails, e.GetEmail()) 74 | } 75 | } 76 | 77 | var teams []string 78 | if as.Config.EnableTeams { 79 | rawTeams, _, _ := client.Teams.ListUserTeams(ctx, nil) 80 | for _, t := range rawTeams { 81 | teams = append(teams, t.GetURL()) 82 | } 83 | } 84 | 85 | return &common.AuthenticateResponse{ 86 | Known: true, 87 | Username: user.GetLogin(), 88 | Metadata: map[string]string{ 89 | "two-factor-authentication": strconv.FormatBool(user.GetTwoFactorAuthentication()), 90 | "name": user.GetName(), 91 | }, 92 | Emails: emails, 93 | Teams: teams, 94 | }, nil 95 | } 96 | -------------------------------------------------------------------------------- /plugins/github-integration/.gitignore: -------------------------------------------------------------------------------- 1 | github-integration 2 | -------------------------------------------------------------------------------- /plugins/github-integration/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | - plugins/github-repo:lib 7 | srcs: 8 | - "**/*.go" 9 | - "go.mod" 10 | - "go.sum" 11 | env: 12 | - CGO_ENABLED=0 13 | config: 14 | buildFlags: ["-o", "werft-plugin-github-integration"] -------------------------------------------------------------------------------- /plugins/github-integration/README.md: -------------------------------------------------------------------------------- 1 | # GitHub integration 2 | 3 | This plugin provides GitHub integration using a GitHub app. It can trigger builds when a branch is pushed, 4 | pull annotations from a PR, take commands from a PR comment and update the commit status. 5 | 6 | ## Installation 7 | First you must create a GitHub app with the following permissions: 8 | - Deployments: Read & Write 9 | - Issues: Read & Write 10 | - Metadata: Read-only 11 | - Pull Requests: Read & Write 12 | - Commit Status: Read & Write 13 | 14 | subscribing to the following events: 15 | - Meta 16 | - Issue Comment 17 | - Push 18 | - Pull Request 19 | 20 | Once you have created this application, please install it on the repositories you intent to use werft with. 21 | 22 | Then add the following to your werft config file: 23 | ```YAML 24 | plugins: 25 | - name: "github-integration" 26 | type: 27 | - integration 28 | config: 29 | baseURL: https://your-werft-installation-url.com 30 | webhookSecret: choose-a-sensible-secret-here 31 | privateKeyPath: path-to-your/app-private-key.pem 32 | appID: 00000 # appID of your GitHub app 33 | installationID: 0000000 # installation ID of your GitHub app installation 34 | jobProtection: "default-branch" 35 | pullRequestComments: 36 | enabled: true 37 | updateComment: true 38 | requiresOrg: [] 39 | requiresWriteAccess: true 40 | ``` 41 | 42 | ### Job Protection 43 | By default this plugin will pull the job config, werft job and `.werft/` directory from the branch it's building. 44 | If `jobProtection` is set to `default-branch`, those files will be taken from the repository's default branch instead. 45 | 46 | ## PR Commands 47 | This integration plugin listens for comments on PRs to trigger operations in werft. 48 | 49 | ```YAML 50 | # start a werft job for this PR 51 | /werft run 52 | ``` 53 | 54 | ## Commit Checks 55 | For all jobs that carry the `updateGitHubStatus` annotation, werft attempts to add a commit check on the repository pointed to in that annotation. E.g. if the job ran with `updateGitHubStatus=csweichel/werft`, upon completion of that job, this plugin would add a check indiciating job success or failure. 56 | By default, all jobs started using this integration plugin (push events or comments) will carry this annotation. 57 | 58 | In addition to the job success annotation, jobs can add additional checks to commits. Any result posted to the `github` or any channel starting with `github-check-` will become a check on the commit. Examples: 59 | 60 | - ``` 61 | werft log result -d "dev installation" -c github url http://foobar.com 62 | ``` 63 | adds the following check (`Details` points to `http://foobar.com`) 64 | ![](docs/check-screenshot.png) 65 | 66 | - ``` 67 | werft log result -d "dev installation" -c github-check-tests conclusion success 68 | ``` 69 | would add a successful check named `continuous-integration/werft/result-tests`, whereas 70 | ``` 71 | werft log result -d "dev installation" -c github-check-tests conclusion failure 72 | ``` 73 | would add a failed check named `continuous-integration/werft/result-tests`. 74 | 75 | Valid values for `conclusion` results in this case are listed in the [GitHub API docs](https://docs.github.com/en/rest/reference/checks#update-a-check-run). -------------------------------------------------------------------------------- /plugins/github-integration/docs/check-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/plugins/github-integration/docs/check-screenshot.png -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/handleCommandRun_args.golden: -------------------------------------------------------------------------------- 1 | { 2 | "req": { 3 | "metadata": { 4 | "owner": "csweichel", 5 | "repository": { 6 | "host": "github.com", 7 | "owner": "csweichel", 8 | "repo": "test-repo", 9 | "ref": "refs/heads/werft", 10 | "revision": "37cb8b8f7fc3499bdf65d662793dd2d968ae3bac", 11 | "defaultBranch": "master" 12 | }, 13 | "trigger": "TRIGGER_MANUAL", 14 | "annotations": [ 15 | { 16 | "key": "arg1" 17 | }, 18 | { 19 | "key": "foo", 20 | "value": "bar" 21 | }, 22 | { 23 | "key": "updateGitHubStatus", 24 | "value": "csweichel/test-repo" 25 | } 26 | ] 27 | }, 28 | "spec": { 29 | "jobPath": "" 30 | } 31 | }, 32 | "msg": "started the job as [foo](/job/foo)" 33 | } -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/handleCommandRun_defaultbranch.golden: -------------------------------------------------------------------------------- 1 | { 2 | "req": { 3 | "metadata": { 4 | "owner": "csweichel", 5 | "repository": { 6 | "host": "github.com", 7 | "owner": "csweichel", 8 | "repo": "test-repo", 9 | "ref": "refs/heads/werft", 10 | "revision": "37cb8b8f7fc3499bdf65d662793dd2d968ae3bac", 11 | "defaultBranch": "master" 12 | }, 13 | "trigger": "TRIGGER_MANUAL", 14 | "annotations": [ 15 | { 16 | "key": "updateGitHubStatus", 17 | "value": "csweichel/test-repo" 18 | } 19 | ] 20 | }, 21 | "spec": { 22 | "repo": { 23 | "repo": { 24 | "host": "github.com", 25 | "owner": "csweichel", 26 | "repo": "test-repo", 27 | "ref": "refs/heads/master", 28 | "defaultBranch": "master" 29 | } 30 | }, 31 | "repoSideload": [ 32 | { 33 | "repo": { 34 | "host": "github.com", 35 | "owner": "csweichel", 36 | "repo": "test-repo", 37 | "ref": "refs/heads/master", 38 | "defaultBranch": "master" 39 | }, 40 | "path": ".werft" 41 | } 42 | ] 43 | } 44 | }, 45 | "msg": "started the job as [foo](/job/foo)\n(with `.werft/` from `master`)" 46 | } -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/handleCommandRun_success.golden: -------------------------------------------------------------------------------- 1 | { 2 | "req": { 3 | "metadata": { 4 | "owner": "csweichel", 5 | "repository": { 6 | "host": "github.com", 7 | "owner": "csweichel", 8 | "repo": "test-repo", 9 | "ref": "refs/heads/werft", 10 | "revision": "37cb8b8f7fc3499bdf65d662793dd2d968ae3bac", 11 | "defaultBranch": "master" 12 | }, 13 | "trigger": "TRIGGER_MANUAL", 14 | "annotations": [ 15 | { 16 | "key": "updateGitHubStatus", 17 | "value": "csweichel/test-repo" 18 | } 19 | ] 20 | }, 21 | "spec": { 22 | "jobPath": "" 23 | } 24 | }, 25 | "msg": "started the job as [foo](/job/foo)" 26 | } -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/processPullRequestEvent_noMD.golden: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "owner": "csweichel", 4 | "repository": { 5 | "host": "github.com", 6 | "owner": "csweichel", 7 | "repo": "test-repo", 8 | "ref": "refs/heads/cannot-init", 9 | "revision": "eed6dfe4dad7e3519acf52679b9880c5d2a2afbf", 10 | "defaultBranch": "master" 11 | }, 12 | "trigger": "TRIGGER_MANUAL", 13 | "annotations": [ 14 | { 15 | "key": "updateGitHubStatus", 16 | "value": "csweichel/test-repo" 17 | } 18 | ] 19 | }, 20 | "spec": { 21 | "jobPath": "" 22 | } 23 | } -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/processPullRequestEvent_noRerun.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csweichel/werft/9311b92c2061c7086f7bc330cc966d45b8f2f08d/plugins/github-integration/fixtures/processPullRequestEvent_noRerun.golden -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/processPullRequestEvent_rerun.golden: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "owner": "csweichel", 4 | "repository": { 5 | "host": "github.com", 6 | "owner": "csweichel", 7 | "repo": "test-repo", 8 | "ref": "refs/heads/cannot-init", 9 | "revision": "eed6dfe4dad7e3519acf52679b9880c5d2a2afbf", 10 | "defaultBranch": "master" 11 | }, 12 | "trigger": "TRIGGER_MANUAL", 13 | "annotations": [ 14 | { 15 | "key": "updateGitHubStatus", 16 | "value": "csweichel/test-repo" 17 | }, 18 | { 19 | "key": "with-preview" 20 | } 21 | ] 22 | }, 23 | "spec": { 24 | "jobPath": "" 25 | } 26 | } -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/processPullRequestEvent_withMD.golden: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "owner": "csweichel", 4 | "repository": { 5 | "host": "github.com", 6 | "owner": "csweichel", 7 | "repo": "test-repo", 8 | "ref": "refs/heads/cannot-init", 9 | "revision": "eed6dfe4dad7e3519acf52679b9880c5d2a2afbf", 10 | "defaultBranch": "master" 11 | }, 12 | "trigger": "TRIGGER_MANUAL", 13 | "annotations": [ 14 | { 15 | "key": "updateGitHubStatus", 16 | "value": "csweichel/test-repo" 17 | }, 18 | { 19 | "key": "with-preview" 20 | } 21 | ] 22 | }, 23 | "spec": { 24 | "jobPath": "" 25 | } 26 | } -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/processPushEvent_delete.golden: -------------------------------------------------------------------------------- 1 | {"metadata":{"owner":"csweichel","repository":{"host":"github.com","owner":"csweichel","repo":"test-repo","ref":"refs/heads/cw/tbd","defaultBranch":"master"},"trigger":"TRIGGER_DELETED"},"spec":{"repo":{"repo":{"host":"github.com","owner":"csweichel","repo":"test-repo","ref":"refs/heads/master","defaultBranch":"master"}},"repoSideload":[{"repo":{"host":"github.com","owner":"csweichel","repo":"test-repo","ref":"refs/heads/master","defaultBranch":"master"},"path":".werft"}]}} -------------------------------------------------------------------------------- /plugins/github-integration/fixtures/processPushEvent_push.golden: -------------------------------------------------------------------------------- 1 | {"metadata":{"owner":"csweichel","repository":{"host":"github.com","owner":"csweichel","repo":"test-repo","ref":"cw/tbd","defaultBranch":"master"},"trigger":"TRIGGER_PUSH","annotations":[{"key":"updateGitHubStatus","value":"csweichel/test-repo"}]},"spec":{"jobPath":""}} -------------------------------------------------------------------------------- /plugins/github-repo/.gitignore: -------------------------------------------------------------------------------- 1 | github-repo -------------------------------------------------------------------------------- /plugins/github-repo/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | srcs: 7 | - "**/*.go" 8 | - "go.mod" 9 | - "go.sum" 10 | env: 11 | - CGO_ENABLED=0 12 | config: 13 | buildFlags: ["-o", "werft-plugin-github-repo"] 14 | - name: lib 15 | type: go 16 | deps: 17 | - //:plugin-client-lib 18 | srcs: 19 | - "pkg/provider/*.go" 20 | - "go.mod" 21 | - "go.sum" 22 | config: 23 | packaging: library 24 | env: 25 | - CGO_ENABLED=0 -------------------------------------------------------------------------------- /plugins/github-repo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/csweichel/werft/plugins/github-repo 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/bradleyfalzon/ghinstallation v1.1.1 7 | github.com/csweichel/werft v0.0.0-00010101000000-000000000000 8 | github.com/google/go-cmp v0.5.7 9 | github.com/google/go-github/v31 v31.0.0 10 | github.com/sirupsen/logrus v1.8.1 11 | google.golang.org/grpc v1.45.0 12 | k8s.io/api v0.23.4 13 | ) 14 | 15 | require ( 16 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 17 | github.com/go-logr/logr v1.2.3 // indirect 18 | github.com/gogo/protobuf v1.3.2 // indirect 19 | github.com/golang/protobuf v1.5.2 // indirect 20 | github.com/google/go-github/v29 v29.0.2 // indirect 21 | github.com/google/go-querystring v1.0.0 // indirect 22 | github.com/google/gofuzz v1.1.0 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 27 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 28 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 29 | golang.org/x/text v0.3.7 // indirect 30 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 31 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 32 | google.golang.org/protobuf v1.28.0 // indirect 33 | gopkg.in/inf.v0 v0.9.1 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 36 | k8s.io/apimachinery v0.23.4 // indirect 37 | k8s.io/klog/v2 v2.30.0 // indirect 38 | k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect 39 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 40 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 41 | ) 42 | 43 | replace github.com/csweichel/werft => ../.. // leeway 44 | 45 | replace k8s.io/api => k8s.io/api v0.23.4 // leeway indirect from //:plugin-client-lib 46 | 47 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 48 | 49 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.23.4 // leeway indirect from //:plugin-client-lib 50 | 51 | replace k8s.io/apiserver => k8s.io/apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 52 | 53 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.4 // leeway indirect from //:plugin-client-lib 54 | 55 | replace k8s.io/client-go => k8s.io/client-go v0.23.4 // leeway indirect from //:plugin-client-lib 56 | 57 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.4 // leeway indirect from //:plugin-client-lib 58 | 59 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.4 // leeway indirect from //:plugin-client-lib 60 | 61 | replace k8s.io/code-generator => k8s.io/code-generator v0.23.4 // leeway indirect from //:plugin-client-lib 62 | 63 | replace k8s.io/component-base => k8s.io/component-base v0.23.4 // leeway indirect from //:plugin-client-lib 64 | 65 | replace k8s.io/cri-api => k8s.io/cri-api v0.23.4 // leeway indirect from //:plugin-client-lib 66 | 67 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.4 // leeway indirect from //:plugin-client-lib 68 | 69 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.4 // leeway indirect from //:plugin-client-lib 70 | 71 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 72 | 73 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.4 // leeway indirect from //:plugin-client-lib 74 | 75 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.4 // leeway indirect from //:plugin-client-lib 76 | 77 | replace k8s.io/kubelet => k8s.io/kubelet v0.23.4 // leeway indirect from //:plugin-client-lib 78 | 79 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.4 // leeway indirect from //:plugin-client-lib 80 | 81 | replace k8s.io/metrics => k8s.io/metrics v0.23.4 // leeway indirect from //:plugin-client-lib 82 | 83 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 84 | 85 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.23.4 // leeway indirect from //:plugin-client-lib 86 | 87 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 88 | 89 | replace k8s.io/kubectl => k8s.io/kubectl v0.23.4 // leeway indirect from //:plugin-client-lib 90 | 91 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.23.4 // leeway indirect from //:plugin-client-lib 92 | -------------------------------------------------------------------------------- /plugins/github-repo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "reflect" 9 | 10 | plugin "github.com/csweichel/werft/pkg/plugin/client" 11 | "github.com/csweichel/werft/pkg/plugin/common" 12 | "github.com/csweichel/werft/plugins/github-repo/pkg/provider" 13 | 14 | "github.com/bradleyfalzon/ghinstallation" 15 | "github.com/google/go-github/v31/github" 16 | ) 17 | 18 | // Config configures this plugin 19 | type Config struct { 20 | PrivateKeyPath string `yaml:"privateKeyPath"` 21 | InstallationID int64 `yaml:"installationID,omitempty"` 22 | AppID int64 `yaml:"appID"` 23 | 24 | ContainerImage string `yaml:"containerImage"` 25 | } 26 | 27 | func main() { 28 | plugin.Serve(&Config{}, 29 | plugin.WithRepositoryPlugin(&githubRepoPlugin{}), 30 | ) 31 | fmt.Fprintln(os.Stderr, "shutting down") 32 | } 33 | 34 | type githubRepoPlugin struct{} 35 | 36 | func (*githubRepoPlugin) Run(ctx context.Context, config interface{}) (common.RepositoryPluginServer, error) { 37 | cfg, ok := config.(*Config) 38 | if !ok { 39 | return nil, fmt.Errorf("config has wrong type %s", reflect.TypeOf(config)) 40 | } 41 | 42 | ghtr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, cfg.AppID, cfg.InstallationID, cfg.PrivateKeyPath) 43 | if err != nil { 44 | return nil, err 45 | } 46 | ghClient := github.NewClient(&http.Client{Transport: ghtr}) 47 | 48 | return &provider.GithubRepoServer{ 49 | Client: ghClient, 50 | Auth: func(ctx context.Context) (user string, pass string, err error) { 51 | tkn, err := ghtr.Token(ctx) 52 | if err != nil { 53 | return 54 | } 55 | user = "x-access-token" 56 | pass = tkn 57 | return 58 | }, 59 | Config: provider.Config{ 60 | ContainerImage: cfg.ContainerImage, 61 | }, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /plugins/github-repo/pkg/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestParseAnnotations(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | Input string 13 | Expected map[string]string 14 | }{ 15 | { 16 | Name: "empty string", 17 | Input: "", 18 | }, 19 | { 20 | Name: "unrelated content", 21 | Input: "Something unrelated", 22 | }, 23 | { 24 | Name: "werft annotation", 25 | Input: "/werft foobar", 26 | Expected: map[string]string{"foobar": ""}, 27 | }, 28 | { 29 | Name: "werft annotation with value", 30 | Input: "/werft foobar=value", 31 | Expected: map[string]string{"foobar": "value"}, 32 | }, 33 | { 34 | Name: "werft annotation with checkbox", 35 | Input: "- [x] /werft foobar", 36 | Expected: map[string]string{"foobar": ""}, 37 | }, 38 | { 39 | Name: "werft annotation with checkbox", 40 | Input: "- [x] /werft foobar=value", 41 | Expected: map[string]string{"foobar": "value"}, 42 | }, 43 | { 44 | Name: "werft annotation with unchecked list checkbox", 45 | Input: "- [ ] /werft foobar", 46 | }, 47 | { 48 | Name: "mixed werft annotation", 49 | Input: "hello world\n /werft foo=bar", 50 | Expected: map[string]string{"foo": "bar"}, 51 | }, 52 | { 53 | Name: "werft annotation with complex value", 54 | Input: "/werft foobar=this=is=another/value 12,3,4,5", 55 | Expected: map[string]string{"foobar": "this=is=another/value 12,3,4,5"}, 56 | }, 57 | { 58 | Name: "werft annotation with empty value", 59 | Input: "/werft foobar=", 60 | Expected: map[string]string{"foobar": ""}, 61 | }, 62 | } 63 | 64 | for _, test := range tests { 65 | t.Run(test.Name, func(t *testing.T) { 66 | res := ParseAnnotations(test.Input) 67 | if diff := cmp.Diff(test.Expected, res); diff != "" { 68 | t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugins/integration-example/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | srcs: 7 | - "**/*.go" 8 | - "go.mod" 9 | - "go.sum" 10 | env: 11 | - CGO_ENABLED=0 12 | config: 13 | buildFlags: ["-o", "werft-plugin-integration-example"] -------------------------------------------------------------------------------- /plugins/integration-example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/csweichel/werft/plugins/integration-example 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/csweichel/werft v0.0.0-00010101000000-000000000000 7 | github.com/sirupsen/logrus v1.8.1 8 | ) 9 | 10 | require ( 11 | github.com/golang/protobuf v1.5.2 // indirect 12 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 13 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 14 | golang.org/x/text v0.3.7 // indirect 15 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 16 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 17 | google.golang.org/grpc v1.45.0 // indirect 18 | google.golang.org/protobuf v1.28.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 20 | ) 21 | 22 | replace github.com/csweichel/werft => ../.. // leeway 23 | 24 | replace k8s.io/api => k8s.io/api v0.23.4 // leeway indirect from //:plugin-client-lib 25 | 26 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 27 | 28 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.23.4 // leeway indirect from //:plugin-client-lib 29 | 30 | replace k8s.io/apiserver => k8s.io/apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 31 | 32 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.4 // leeway indirect from //:plugin-client-lib 33 | 34 | replace k8s.io/client-go => k8s.io/client-go v0.23.4 // leeway indirect from //:plugin-client-lib 35 | 36 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.4 // leeway indirect from //:plugin-client-lib 37 | 38 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.4 // leeway indirect from //:plugin-client-lib 39 | 40 | replace k8s.io/code-generator => k8s.io/code-generator v0.23.4 // leeway indirect from //:plugin-client-lib 41 | 42 | replace k8s.io/component-base => k8s.io/component-base v0.23.4 // leeway indirect from //:plugin-client-lib 43 | 44 | replace k8s.io/cri-api => k8s.io/cri-api v0.23.4 // leeway indirect from //:plugin-client-lib 45 | 46 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.4 // leeway indirect from //:plugin-client-lib 47 | 48 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.4 // leeway indirect from //:plugin-client-lib 49 | 50 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 51 | 52 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.4 // leeway indirect from //:plugin-client-lib 53 | 54 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.4 // leeway indirect from //:plugin-client-lib 55 | 56 | replace k8s.io/kubelet => k8s.io/kubelet v0.23.4 // leeway indirect from //:plugin-client-lib 57 | 58 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.4 // leeway indirect from //:plugin-client-lib 59 | 60 | replace k8s.io/metrics => k8s.io/metrics v0.23.4 // leeway indirect from //:plugin-client-lib 61 | 62 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 63 | 64 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.23.4 // leeway indirect from //:plugin-client-lib 65 | 66 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 67 | 68 | replace k8s.io/kubectl => k8s.io/kubectl v0.23.4 // leeway indirect from //:plugin-client-lib 69 | 70 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.23.4 // leeway indirect from //:plugin-client-lib 71 | -------------------------------------------------------------------------------- /plugins/integration-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | v1 "github.com/csweichel/werft/pkg/api/v1" 9 | "github.com/csweichel/werft/pkg/plugin/client" 10 | plugin "github.com/csweichel/werft/pkg/plugin/client" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Config configures this plugin 15 | type Config struct { 16 | Emoji string `yaml:"emoji"` 17 | } 18 | 19 | func main() { 20 | plugin.Serve(&Config{}, 21 | plugin.WithIntegrationPlugin(&integrationPlugin{}), 22 | ) 23 | } 24 | 25 | type integrationPlugin struct{} 26 | 27 | func (*integrationPlugin) Run(ctx context.Context, config interface{}, srv *client.Services) error { 28 | cfg, ok := config.(*Config) 29 | if !ok { 30 | return fmt.Errorf("config has wrong type %s", reflect.TypeOf(config)) 31 | } 32 | 33 | sub, err := srv.Subscribe(ctx, &v1.SubscribeRequest{}) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | log.Infof("hello world %s", cfg.Emoji) 39 | for { 40 | resp, err := sub.Recv() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | fmt.Printf("%s %v\n", cfg.Emoji, resp) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /plugins/otel-exporter/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | srcs: 7 | - "main.go" 8 | - "go.mod" 9 | - "go.sum" 10 | env: 11 | - CGO_ENABLED=0 12 | config: 13 | buildFlags: ["-o", "werft-plugin-otel-exporter"] -------------------------------------------------------------------------------- /plugins/otel-exporter/README.md: -------------------------------------------------------------------------------- 1 | This plugin emits OpenTelemetry tracing data for werft builds. 2 | 3 | ## Configuration 4 | ```YAML 5 | # which OTel exporter to use. Supported values are "stdout" and "http" 6 | exporter: "http" 7 | ``` 8 | 9 | When using the `http` exporter, you can configure its behaviour using the `OTEL` environment variables, e.g. 10 | ```bash 11 | export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io/" 12 | export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=your-api-key,x-honeycomb-dataset=your-dataset" 13 | export OTEL_SERVICE_NAME="your-service-name" 14 | ``` 15 | -------------------------------------------------------------------------------- /plugins/otel-exporter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/csweichel/werft/plugins/otel-exporter 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/csweichel/werft v0.0.0-00010101000000-000000000000 7 | github.com/golang/mock v1.5.0 8 | github.com/sirupsen/logrus v1.8.1 9 | go.opentelemetry.io/otel v1.7.0 10 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 11 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.7.0 12 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.7.0 13 | go.opentelemetry.io/otel/sdk v1.7.0 14 | go.opentelemetry.io/otel/trace v1.7.0 15 | google.golang.org/protobuf v1.28.0 16 | ) 17 | 18 | require ( 19 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 20 | github.com/go-logr/logr v1.2.3 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/golang/protobuf v1.5.2 // indirect 23 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect 24 | go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect 25 | go.opentelemetry.io/proto/otlp v0.16.0 // indirect 26 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 27 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 28 | golang.org/x/text v0.3.7 // indirect 29 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 30 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 31 | google.golang.org/grpc v1.46.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 33 | ) 34 | 35 | replace github.com/csweichel/werft => ../.. // leeway 36 | 37 | replace k8s.io/api => k8s.io/api v0.23.4 // leeway indirect from //:plugin-client-lib 38 | 39 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 40 | 41 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.23.4 // leeway indirect from //:plugin-client-lib 42 | 43 | replace k8s.io/apiserver => k8s.io/apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 44 | 45 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.4 // leeway indirect from //:plugin-client-lib 46 | 47 | replace k8s.io/client-go => k8s.io/client-go v0.23.4 // leeway indirect from //:plugin-client-lib 48 | 49 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.4 // leeway indirect from //:plugin-client-lib 50 | 51 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.4 // leeway indirect from //:plugin-client-lib 52 | 53 | replace k8s.io/code-generator => k8s.io/code-generator v0.23.4 // leeway indirect from //:plugin-client-lib 54 | 55 | replace k8s.io/component-base => k8s.io/component-base v0.23.4 // leeway indirect from //:plugin-client-lib 56 | 57 | replace k8s.io/cri-api => k8s.io/cri-api v0.23.4 // leeway indirect from //:plugin-client-lib 58 | 59 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.4 // leeway indirect from //:plugin-client-lib 60 | 61 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.4 // leeway indirect from //:plugin-client-lib 62 | 63 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 64 | 65 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.4 // leeway indirect from //:plugin-client-lib 66 | 67 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.4 // leeway indirect from //:plugin-client-lib 68 | 69 | replace k8s.io/kubelet => k8s.io/kubelet v0.23.4 // leeway indirect from //:plugin-client-lib 70 | 71 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.4 // leeway indirect from //:plugin-client-lib 72 | 73 | replace k8s.io/metrics => k8s.io/metrics v0.23.4 // leeway indirect from //:plugin-client-lib 74 | 75 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 76 | 77 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.23.4 // leeway indirect from //:plugin-client-lib 78 | 79 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 80 | 81 | replace k8s.io/kubectl => k8s.io/kubectl v0.23.4 // leeway indirect from //:plugin-client-lib 82 | 83 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.23.4 // leeway indirect from //:plugin-client-lib 84 | -------------------------------------------------------------------------------- /plugins/otel-exporter/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "testing" 8 | "time" 9 | 10 | v1 "github.com/csweichel/werft/pkg/api/v1" 11 | "github.com/csweichel/werft/pkg/api/v1/mock" 12 | "github.com/csweichel/werft/pkg/logcutter" 13 | "github.com/csweichel/werft/pkg/plugin/client" 14 | "github.com/golang/mock/gomock" 15 | "google.golang.org/protobuf/types/known/timestamppb" 16 | 17 | _ "embed" 18 | ) 19 | 20 | func TestOtelExporterPlugin(t *testing.T) { 21 | ctrl := gomock.NewController(t) 22 | defer ctrl.Finish() 23 | 24 | var ( 25 | jobName = "test-job" 26 | jobMD = &v1.JobMetadata{ 27 | Owner: "someone", 28 | Repository: &v1.Repository{}, 29 | Trigger: v1.JobTrigger_TRIGGER_MANUAL, 30 | Created: timestamppb.New(time.UnixMilli(0)), 31 | } 32 | ) 33 | 34 | var statusIdx int 35 | status := []*v1.JobStatus{ 36 | { 37 | Name: jobName, 38 | Metadata: jobMD, 39 | Phase: v1.JobPhase_PHASE_PREPARING, 40 | }, 41 | } 42 | 43 | sub := mock.NewMockWerftService_SubscribeClient(ctrl) 44 | sub.EXPECT().Recv().DoAndReturn(func() (*v1.SubscribeResponse, error) { 45 | if statusIdx >= len(status) { 46 | time.Sleep(1 * time.Millisecond) 47 | return nil, io.EOF 48 | } 49 | res := &v1.SubscribeResponse{Result: status[statusIdx]} 50 | statusIdx++ 51 | return res, nil 52 | }).AnyTimes() 53 | 54 | logevt, errchan := logcutter.DefaultCutter.Slice(bytes.NewReader([]byte(logtext))) 55 | 56 | logsClient := mock.NewMockWerftService_ListenClient(ctrl) 57 | logsClient.EXPECT().Recv().DoAndReturn(func() (*v1.ListenResponse, error) { 58 | select { 59 | case err := <-errchan: 60 | return nil, err 61 | case evt := <-logevt: 62 | if evt != nil && evt.Type != v1.LogSliceType_SLICE_CONTENT { 63 | time.Sleep(1 * time.Millisecond) 64 | t.Log(evt) 65 | } 66 | return &v1.ListenResponse{Content: &v1.ListenResponse_Slice{Slice: evt}}, nil 67 | } 68 | }).AnyTimes() 69 | 70 | werftClient := mock.NewMockWerftServiceClient(ctrl) 71 | werftClient.EXPECT().Subscribe(gomock.Any(), gomock.Any()).Return(sub, nil) 72 | werftClient.EXPECT().Listen(gomock.Any(), gomock.Any()).Return(logsClient, nil) 73 | 74 | plugin := &otelExporterPlugin{} 75 | err := plugin.Run(context.Background(), &Config{ 76 | Exporter: OTelExporterStdout, 77 | }, &client.Services{WerftServiceClient: werftClient}) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | } 82 | 83 | //go:embed example-log.txt 84 | var logtext string 85 | -------------------------------------------------------------------------------- /plugins/webhook/BUILD.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: app 3 | type: go 4 | deps: 5 | - //:plugin-client-lib 6 | srcs: 7 | - "**/*.go" 8 | - "go.mod" 9 | - "go.sum" 10 | env: 11 | - CGO_ENABLED=0 12 | config: 13 | buildFlags: ["-o", "werft-plugin-webhook"] -------------------------------------------------------------------------------- /plugins/webhook/README.md: -------------------------------------------------------------------------------- 1 | This plugin HTTP POST's messages to a URL. 2 | For example, to send messages to a Slack webhook: 3 | ```YAML 4 | url: https://your-webhook.slack.com 5 | contentType: application/json 6 | filter: 7 | - phase==done 8 | template: '{"text": "{{ .Name }} done"}' 9 | ``` 10 | 11 | The template receives a [JobStatus](https://godoc.org/github.com/csweichel/werft/pkg/api/v1#JobStatus) as context. -------------------------------------------------------------------------------- /plugins/webhook/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/csweichel/werft/plugins/webhook 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/csweichel/werft v0.0.0-00010101000000-000000000000 7 | github.com/sirupsen/logrus v1.8.1 8 | ) 9 | 10 | require ( 11 | github.com/golang/protobuf v1.5.2 // indirect 12 | golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect 13 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 14 | golang.org/x/text v0.3.7 // indirect 15 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 16 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 17 | google.golang.org/grpc v1.45.0 // indirect 18 | google.golang.org/protobuf v1.28.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 20 | ) 21 | 22 | replace github.com/csweichel/werft => ../.. // leeway 23 | 24 | replace k8s.io/api => k8s.io/api v0.23.4 // leeway indirect from //:plugin-client-lib 25 | 26 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 27 | 28 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.23.4 // leeway indirect from //:plugin-client-lib 29 | 30 | replace k8s.io/apiserver => k8s.io/apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 31 | 32 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.4 // leeway indirect from //:plugin-client-lib 33 | 34 | replace k8s.io/client-go => k8s.io/client-go v0.23.4 // leeway indirect from //:plugin-client-lib 35 | 36 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.4 // leeway indirect from //:plugin-client-lib 37 | 38 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.4 // leeway indirect from //:plugin-client-lib 39 | 40 | replace k8s.io/code-generator => k8s.io/code-generator v0.23.4 // leeway indirect from //:plugin-client-lib 41 | 42 | replace k8s.io/component-base => k8s.io/component-base v0.23.4 // leeway indirect from //:plugin-client-lib 43 | 44 | replace k8s.io/cri-api => k8s.io/cri-api v0.23.4 // leeway indirect from //:plugin-client-lib 45 | 46 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.4 // leeway indirect from //:plugin-client-lib 47 | 48 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.4 // leeway indirect from //:plugin-client-lib 49 | 50 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 51 | 52 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.4 // leeway indirect from //:plugin-client-lib 53 | 54 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.4 // leeway indirect from //:plugin-client-lib 55 | 56 | replace k8s.io/kubelet => k8s.io/kubelet v0.23.4 // leeway indirect from //:plugin-client-lib 57 | 58 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.4 // leeway indirect from //:plugin-client-lib 59 | 60 | replace k8s.io/metrics => k8s.io/metrics v0.23.4 // leeway indirect from //:plugin-client-lib 61 | 62 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.4 // leeway indirect from //:plugin-client-lib 63 | 64 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.23.4 // leeway indirect from //:plugin-client-lib 65 | 66 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.23.4 // leeway indirect from //:plugin-client-lib 67 | 68 | replace k8s.io/kubectl => k8s.io/kubectl v0.23.4 // leeway indirect from //:plugin-client-lib 69 | 70 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.23.4 // leeway indirect from //:plugin-client-lib 71 | -------------------------------------------------------------------------------- /plugins/webhook/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "net/http" 10 | "reflect" 11 | "sync" 12 | "time" 13 | 14 | v1 "github.com/csweichel/werft/pkg/api/v1" 15 | "github.com/csweichel/werft/pkg/filterexpr" 16 | "github.com/csweichel/werft/pkg/plugin/client" 17 | plugin "github.com/csweichel/werft/pkg/plugin/client" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | // Config configures this plugin 22 | type Config struct { 23 | Notifications []struct { 24 | WebhookURL string `yaml:"url"` 25 | Filter []string `yaml:"filter"` 26 | Template string `yaml:"template"` 27 | ContentType string `yaml:"contentType"` 28 | } `yaml:"notifications"` 29 | } 30 | 31 | func main() { 32 | plugin.Serve(&Config{}, 33 | plugin.WithIntegrationPlugin(&webhookPlugin{}), 34 | ) 35 | } 36 | 37 | type webhookPlugin struct{} 38 | 39 | func (*webhookPlugin) Run(ctx context.Context, config interface{}, srv *client.Services) error { 40 | cfg, ok := config.(*Config) 41 | if !ok { 42 | return fmt.Errorf("config has wrong type %s", reflect.TypeOf(config)) 43 | } 44 | 45 | var wg sync.WaitGroup 46 | for idx, nf := range cfg.Notifications { 47 | filter, err := filterexpr.Parse(nf.Filter) 48 | if err != nil { 49 | log.WithError(err).Errorf("cannot parse filter for notification %d", idx) 50 | } 51 | 52 | tpl, err := template.New("tpl").Parse(nf.Template) 53 | if err != nil { 54 | log.WithError(err).Errorf("cannot parse template for notification %d", idx) 55 | } 56 | 57 | wg.Add(1) 58 | go func(idx int, url string, contentType string, tpl *template.Template) { 59 | defer wg.Done() 60 | 61 | sub, err := srv.Subscribe(ctx, &v1.SubscribeRequest{ 62 | Filter: []*v1.FilterExpression{{Terms: filter}}, 63 | }) 64 | if err != nil { 65 | log.WithError(err).Errorf("cannot subscribe for notification %d", idx) 66 | return 67 | } 68 | log.Infof("notifications for %s set up", url) 69 | 70 | for { 71 | resp, err := sub.Recv() 72 | if err != nil { 73 | log.WithError(err).Errorf("subscription error with notification %d", idx) 74 | return 75 | } 76 | 77 | buf := bytes.NewBuffer(nil) 78 | err = tpl.Execute(buf, resp.Result) 79 | if err != nil { 80 | log.WithError(err).Warnf("template error with notification %d", idx) 81 | continue 82 | } 83 | 84 | err = sendNotification(url, contentType, buf) 85 | if err != nil { 86 | log.WithError(err).Warnf("send error with notification %d", idx) 87 | continue 88 | } 89 | } 90 | }(idx, nf.WebhookURL, nf.ContentType, tpl) 91 | } 92 | 93 | wg.Wait() 94 | return nil 95 | } 96 | 97 | // sendNotification will post text to a URL 98 | func sendNotification(webhookURL string, contentType string, body io.Reader) error { 99 | log.WithField("url", webhookURL).Info("sending message") 100 | 101 | req, err := http.NewRequest(http.MethodPost, webhookURL, body) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | req.Header.Add("Content-Type", contentType) 107 | 108 | client := &http.Client{Timeout: 10 * time.Second} 109 | resp, err := client.Do(req) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | buf := new(bytes.Buffer) 115 | buf.ReadFrom(resp.Body) 116 | if buf.String() != "ok" { 117 | return fmt.Errorf("non-ok response") 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euf -o pipefail 3 | 4 | # build web ui 5 | cd pkg/webui 6 | yarn 7 | yarn build 8 | cd - 9 | 10 | # embed webui 11 | go get github.com/GeertJohan/go.rice/rice 12 | rice embed-go -i ./cmd/server 13 | rice embed-go -i ./pkg/store/postgres -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | leeway build //:release -Dcommit=$(git rev-parse HEAD) -Ddate="$(date)" $* 4 | -------------------------------------------------------------------------------- /server.Dockerfile: -------------------------------------------------------------------------------- 1 | # build with leeway build :server-docker 2 | FROM alpine:latest 3 | 4 | COPY plugins--all/bin/* /app/plugins/ 5 | ENV PATH=$PATH:/app/plugins 6 | 7 | COPY server/werft /app/werft 8 | RUN chmod +x /app/werft 9 | ENTRYPOINT [ "/app/werft" ] 10 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | //go:build !client 2 | // +build !client 3 | 4 | // Copyright © 2019 Christian Weichel 5 | 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | package main 25 | 26 | import ( 27 | cmd "github.com/csweichel/werft/cmd/server" 28 | 29 | _ "github.com/csweichel/werft/pkg/webui" 30 | _ "github.com/lib/pq" 31 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 32 | ) 33 | 34 | func main() { 35 | cmd.Execute() 36 | } 37 | -------------------------------------------------------------------------------- /testdata/credential-helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo $GITHUB_TOKEN 4 | timeout 1s cat - > /tmp/werft-debug.json -------------------------------------------------------------------------------- /testdata/example-app.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA70eU/a/Z0FVGSQYG/GIg8cZbjEmf1uGtlBk4hqp/F4BOLZXt 3 | OBsU1YOyLG143L1JCbholks108vM9LOliL5KR/Eju92Ee5t0+8ktYbJ7+OOqsvqL 4 | t16BCu5Fq78V6BuQHKjWaJ3vGBx5ORV+Gyi3UrYx1gTTz3xynQM+ZQYVQj99EV+j 5 | qySSIBsLR8D+GKrlPUrZ9VuyYsmsmn7E4Mx+/zC4/F+tQBBIciyy36CCxnSapBj0 6 | shGltdLmEO2NJXeZdXGzqXziPZgtWekosyLQ1UqDIeNb8cVDfQvGwWpSt+FPoqBO 7 | GDcXEPq4hmRjB7fG8k4/70xwH+xd/F6fFftbNwIDAQABAoIBAEopDWxzDDcdtuL7 8 | Ez81yrAkoksgpoGbAIleJ77VKP7HrXNDfHpfKl3iq15Jr6P6pqB0nzW1qcEy8RsG 9 | cs+m6q7RdhnL1jvZOrCu8XnOL848AbPnI1Z529TfdIh+ePOvV0MKsSlLiccXTBr9 10 | JlCUlfz0qw2CAYPVNlCjqLr236/gFB13UexYzoC+SWMlX0Tsfip78Lg0eZ4QCLUU 11 | ajKh0ikPnNJwa3BLqb3JdRZgIJRNzUWxB7kgrYxrBdSrAE1jMiyv/5Y+WhdXPSxi 12 | zc2+dx9iRCcz9joB1BvMzP6JkIBEfY3+ihugr+gjnL5cPvIUTkmQr2h/FP4qRvkz 13 | apBqfwECgYEA+oHR9pIp5EDqSDtbLLqiteElNntYpu0ZlPSOhuj58ZiI67WMXtic 14 | 7HCmwY9SxyXxGokHdbfn68nvbVZHsm/iHX8f+x6ySkQw9Z7q9I1FzlvXZWm0ylt9 15 | rby0z/vsjoARVDPxbD0DPXR/BJQcgbJoQX3pMbEfmQMgpx696tHS2vECgYEA9Ia8 16 | +MXDMDjD8U9c0FSBvlh9nslMHxbJ62vl94B+TSNH11VhGI2s5++JIgPpbFIxAQUC 17 | M+9Ew7z/K7N6iXAR6Q7+leE55w0/1paYPWrGdsFSZ04CcFb1ouOEU32E1iRkBha1 18 | 1olLs5bK5De8+XO4LNq/SJtbwavHHMISY8IHCKcCgYBYHQnJfSgXDW5a8eXkGdHZ 19 | v9PjEgfgz01MQ6lOcuxXupuOrVEum2q3D/jX5J3tRr9D4icplQKSwXjiMJMPhKM4 20 | VNre7bEwxkOiYb+rPXXsXAmrtj7NXtkaH2JKNgbDKPDveUXWGK/nEe8LoT1VsXdS 21 | cgNwYykGHT+DCSEsU5mjQQKBgAGxpUF77Tw6SHE1gYkX7MYqysP81QAqIj/1QWST 22 | iUxzgB3nw4JuCNKagDKyID3V2+0L4dYGRE2u0320ApdNJXKd3fmf08zb9KNB69AR 23 | G0rbT/zTN4UbtRvpw5LofbEWE3NPWPchgFrAIquuDysTOCVnZofUO7B9xiVW2tXC 24 | FFZlAoGBAItkNJs/5xg4MJMJ+LXMDq/jUoTT5wYyFtnxdKR+R++6gnaWY9RuhAV9 25 | N12tQtz5nsZ8bBWq5L+3rTVjEQSE3mlOnZ11Uyijub4RX3st2Mjxu1C/qV9DH1q7 26 | 2EfFLhJyp77O5ZIMZeaBxUONydw16XUuwTJSDSd1XEfuIaJ8hvLQ 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /testdata/example-config.yaml: -------------------------------------------------------------------------------- 1 | werft: 2 | baseURL: https://werft.com 3 | workspaceNodePathPrefix: "/mnt/disks/ssd0/builds" 4 | service: 5 | webPort: 8080 6 | grpcPort: 7777 7 | jobSpecRepos: 8 | - 32leaves/test-repo:werft 9 | kubeconfig: "/Users/csweichel/.kube/config" 10 | executor: 11 | preperationTimeout: 10m 12 | totalTimeout: 60m 13 | storage: 14 | logsPath: "/tmp/logs" 15 | jobsConnectionString: dbname=werft user=postgres connect_timeout=5 sslmode=disable 16 | github: 17 | webhookSecret: foobar 18 | privateKeyPath: testdata/example-app.pem 19 | appID: 48144 20 | installationID: 5647067 -------------------------------------------------------------------------------- /testdata/in-gitpod-config.yaml: -------------------------------------------------------------------------------- 1 | werft: 2 | baseURL: https://werft.dev 3 | workspaceNodePathPrefix: "/mnt/disks/ssd0/builds" 4 | gcOlderThan: "10m" 5 | cleanupJobSpec: 6 | terminationGracePeriodSeconds: 10 7 | service: 8 | webPort: 8080 9 | grpcPort: 7777 10 | prometheusPort: 9500 11 | pprofPort: 6060 12 | jobSpecRepos: 13 | - github.com/csweichel/test-repo:werft 14 | webReadOnly: false 15 | apiPolicy: 16 | enabled: true 17 | paths: 18 | - testdata/policy/api.rego 19 | kubeconfig: "/home/gitpod/.kube/k3s.yaml" 20 | executor: 21 | preperationTimeout: 10m 22 | totalTimeout: 60m 23 | storage: 24 | logsPath: "/tmp/logs" 25 | jobsConnectionString: dbname=werft user=gitpod connect_timeout=5 sslmode=disable 26 | jobsMaxConnections: 10 27 | plugins: 28 | - name: "github-repo" 29 | type: 30 | - repository 31 | config: 32 | privateKeyPath: testdata/example-app.pem 33 | appID: 48144 34 | installationID: 5647067 35 | command: 36 | - sh 37 | - -c 38 | - cd plugins/github-repo; go build; cd -; plugins/github-repo/github-repo $* 39 | - -s 40 | - name: "github-auth" 41 | type: 42 | - auth 43 | config: 44 | enableEmails: true 45 | enableTeams: false 46 | command: 47 | - sh 48 | - -c 49 | - cd plugins/github-auth; go run main.go $* 50 | - -s 51 | - name: "github-integration" 52 | type: 53 | - integration 54 | config: 55 | baseURL: https://werft.dev 56 | webhookSecret: foobar 57 | privateKeyPath: testdata/example-app.pem 58 | appID: 48144 59 | installationID: 5647067 60 | jobProtection: "default-branch" 61 | pullRequestComments: 62 | enabled: true 63 | updateComment: true 64 | requiresOrg: [] 65 | requiresWriteAccess: true 66 | command: 67 | - sh 68 | - -c 69 | - cd plugins/github-integration; go build; cd -; plugins/github-integration/github-integration $* 70 | - -s 71 | - name: "example" 72 | type: 73 | - integration 74 | config: 75 | emoji: 🚀 76 | command: 77 | - sh 78 | - -c 79 | - cd plugins/integration-example && go run main.go $* 80 | - -s 81 | - name: "webhook" 82 | type: 83 | - integration 84 | config: 85 | notifications: 86 | - url: http://localhost:8081 87 | contentType: application/json 88 | filter: 89 | - phase==done 90 | template: '{"text": "{{ .Name }} done"}' 91 | command: 92 | - sh 93 | - -c 94 | - cd plugins/webhook && go run main.go $* 95 | - -s 96 | - name: "cron" 97 | type: 98 | - integration 99 | config: 100 | tasks: 101 | - spec: "@every 10m" 102 | repo: github.com/csweichel/test-repo:werft 103 | command: 104 | - sh 105 | - -c 106 | - cd plugins/cron && go run main.go $* 107 | - -s 108 | - name: "otel-exporter" 109 | type: 110 | - integration 111 | config: 112 | exporter: stdout 113 | command: 114 | - sh 115 | - -c 116 | - cd plugins/otel-exporter && go run main.go $* 117 | - -s -------------------------------------------------------------------------------- /testdata/policy/api.rego: -------------------------------------------------------------------------------- 1 | package werft 2 | 3 | default allow = false 4 | 5 | allow { 6 | input.method == "/v1.WerftService/ListJobs" 7 | } 8 | allow { 9 | input.method == "/v1.WerftService/Listen" 10 | } 11 | 12 | # Allow running GitHub jobs with sideloading only for team members and not on main 13 | allow { 14 | is_team_member 15 | 16 | input.method == "/v1.WerftService/StartGitHubJob" 17 | input.message.sideload != "" 18 | not startswith(input.message.metadata.repository.ref, "refs/heads/main") 19 | } 20 | # Allow running GitHub jobs with custom jobs only for team members and not on main 21 | allow { 22 | is_team_member 23 | 24 | input.method == "/v1.WerftService/StartGitHubJob" 25 | input.message.job_yaml != "" 26 | not startswith(input.message.metadata.repository.ref, "refs/heads/main") 27 | } 28 | # Allow running GitHub jobs on all branches without sideloading/custom jobs 29 | allow { 30 | is_team_member 31 | 32 | input.method == "/v1.WerftService/StartGitHubJob" 33 | not input.message.job_yaml 34 | not input.message.job_path 35 | not input.message.sideload 36 | } 37 | 38 | # Allow team members to run previously started jobs 39 | allow { 40 | is_team_member 41 | 42 | input.method == "/v1.WerftService/StartFromPreviousJob" 43 | } 44 | 45 | is_team_member { 46 | input.auth.known 47 | endswith(input.auth.emails[_], "@gitpod.io") 48 | } 49 | -------------------------------------------------------------------------------- /testdata/send-push-event.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -XPOST \ 4 | -H"content-type: application/json" \ 5 | -H"Expect: " \ 6 | -H"User-Agent: GitHub-Hookshot/b6210f6" \ 7 | -H"X-GitHub-Delivery: 5529067a-14f1-11ea-8f35-75cb7053287b" \ 8 | -H "X-GitHub-Event: push" \ 9 | -H "X-Hub-Signature: sha1=f6b0ccbd7dbe39d2a807668670e60bd07dbd6b6a" \ 10 | -d @push-event-payload.json \ 11 | http://localhost:8080/plugins/github-trigger -------------------------------------------------------------------------------- /werft.theia-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | {"path": "."}, 4 | {"path": "pkg/webui"}, 5 | {"path": "plugins/cron"}, 6 | {"path": "plugins/github-auth"}, 7 | {"path": "plugins/github-repo"}, 8 | {"path": "plugins/github-integration"}, 9 | {"path": "plugins/integration-example"}, 10 | {"path": "plugins/otel-exporter"}, 11 | {"path": "plugins/webhook"} 12 | ], 13 | "settings": { 14 | "go.useLanguageServer": true 15 | } 16 | } --------------------------------------------------------------------------------