├── docs
├── CNAME
├── img
│ ├── favicon.ico
│ ├── piper-demo-1080.mov
│ └── piper-demo-1080.mp4
├── index.md
├── configuration
│ ├── health_check.md
│ └── environment_variables.md
├── usage
│ ├── workflows_config.md
│ ├── global_variables.md
│ └── workflows_folder.md
├── getting_started
│ └── installation.md
└── CONTRIBUTING.md
├── .github
├── CODEOWNERS
├── workflows
│ ├── lint-commit.yaml
│ ├── release-please.yaml
│ ├── docs.yaml
│ ├── lint-pr.yaml
│ ├── release.yaml
│ ├── ci.yaml
│ ├── snyk.yaml
│ └── e2e.yaml
├── dependabot.yml
└── pull_request_template.md
├── examples
├── .workflows
│ ├── parameters.yaml
│ ├── exit.yaml
│ ├── templates.yaml
│ ├── main.yaml
│ └── triggers.yaml
├── template.values.dev.yaml
├── config.yaml
└── workflow.yaml
├── .dockerignore
├── helm-chart
├── Chart.yaml
├── templates
│ ├── config.yaml
│ ├── serviceaccount.yaml
│ ├── service.yaml
│ ├── rookout-token.yaml
│ ├── git-token-secret.yaml
│ ├── argo-token-secret.yaml
│ ├── webhook-secret.yaml
│ ├── hpa.yaml
│ ├── security.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl
│ └── deployment.yaml
├── .helmignore
├── README.md
└── values.yaml
├── scripts
├── init-piper.sh
├── init-argo-workflows.sh
├── init-nginx.sh
└── init-kind.sh
├── pkg
├── clients
│ └── types.go
├── webhook_creator
│ ├── types.go
│ ├── mocks.go
│ └── main.go
├── server
│ ├── routes
│ │ ├── readyz.go
│ │ ├── healthz.go
│ │ └── webhook.go
│ ├── main.go
│ ├── types.go
│ ├── shutdown.go
│ └── server.go
├── common
│ └── types.go
├── event_handler
│ ├── types.go
│ ├── main.go
│ ├── workflow_event_handler.go
│ ├── github_event_notifier.go
│ └── github_event_notifier_test.go
├── webhook_handler
│ ├── types.go
│ └── webhook_handler.go
├── conf
│ ├── rookout.go
│ ├── conf.go
│ ├── workflow_server.go
│ ├── workflows_config.go
│ └── git_provider.go
├── git_provider
│ ├── main.go
│ ├── types.go
│ ├── bitbucket_utils.go
│ ├── github_utils_test.go
│ ├── test_utils.go
│ ├── github_utils.go
│ ├── bitbucket_test.go
│ └── bitbucket.go
├── workflow_handler
│ ├── types.go
│ ├── workflows_utils.go
│ ├── workflows_test.go
│ ├── workflows_utils_test.go
│ └── workflows.go
└── utils
│ ├── os.go
│ ├── os_test.go
│ └── common.go
├── workflows.values.yaml
├── Dockerfile
├── mkdocs.yml
├── makefile
├── .gitignore
├── cmd
└── piper
│ └── piper.go
├── README.md
├── go.mod
└── LICENSE
/docs/CNAME:
--------------------------------------------------------------------------------
1 | piper.rookout.com
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @gosharo
2 | **/.github @gosharo
--------------------------------------------------------------------------------
/examples/.workflows/parameters.yaml:
--------------------------------------------------------------------------------
1 | - name: global
2 | value: multi-branch-pipeline
--------------------------------------------------------------------------------
/docs/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rookout/piper/HEAD/docs/img/favicon.ico
--------------------------------------------------------------------------------
/docs/img/piper-demo-1080.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rookout/piper/HEAD/docs/img/piper-demo-1080.mov
--------------------------------------------------------------------------------
/docs/img/piper-demo-1080.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rookout/piper/HEAD/docs/img/piper-demo-1080.mp4
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Keep ignoring .git
2 | .git
3 | # Allow specific files with !
4 | !.git/HEAD
5 | !.git/config
6 | !.git/refs
--------------------------------------------------------------------------------
/helm-chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: piper
3 | description: A Helm chart for Piper
4 | type: application
5 | version: 1.0.1
6 | appVersion: 1.0.1
--------------------------------------------------------------------------------
/examples/.workflows/exit.yaml:
--------------------------------------------------------------------------------
1 | - name: github-status
2 | template: exit-handler
3 | arguments:
4 | parameters:
5 | - name: param1
6 | value: "{{ workflow.labels.repo }}"
--------------------------------------------------------------------------------
/scripts/init-piper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -o errexit
3 |
4 | if [ -z "$(helm list | grep piper)" ]; then
5 | # 8. Install Piper
6 | helm upgrade --install piper ./helm-chart -f values.dev.yaml
7 | else
8 | echo "Piper release exists, skipping installation"
9 | fi
10 |
--------------------------------------------------------------------------------
/pkg/clients/types.go:
--------------------------------------------------------------------------------
1 | package clients
2 |
3 | import (
4 | "github.com/rookout/piper/pkg/git_provider"
5 | "github.com/rookout/piper/pkg/workflow_handler"
6 | )
7 |
8 | type Clients struct {
9 | GitProvider git_provider.Client
10 | Workflows workflow_handler.WorkflowsClient
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/webhook_creator/types.go:
--------------------------------------------------------------------------------
1 | package webhook_creator
2 |
3 | import "golang.org/x/net/context"
4 |
5 | type WebhookCreator interface {
6 | Stop(ctx *context.Context)
7 | Start()
8 | SetWebhookHealth(status bool, hookID *int64) error
9 | RunDiagnosis(ctx *context.Context) error
10 | }
11 |
--------------------------------------------------------------------------------
/examples/.workflows/templates.yaml:
--------------------------------------------------------------------------------
1 | - name: local-step
2 | inputs:
3 | parameters:
4 | - name: message
5 | script:
6 | image: alpine
7 | command: [ sh ]
8 | source: |
9 | echo "wellcome to {{ workflow.parameters.global }}
10 | echo "{{ inputs.parameters.message }}"
--------------------------------------------------------------------------------
/pkg/server/routes/readyz.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func AddReadyRoutes(rg *gin.RouterGroup) {
10 | health := rg.Group("/readyz")
11 |
12 | health.GET("", func(c *gin.Context) {
13 | c.JSON(http.StatusOK, "ready")
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/examples/.workflows/main.yaml:
--------------------------------------------------------------------------------
1 | - name: local-step1
2 | template: local-step
3 | arguments:
4 | parameters:
5 | - name: message
6 | value: step-1
7 | - name: local-step2
8 | template: local-step
9 | arguments:
10 | parameters:
11 | - name: message
12 | value: step-2
13 | dependencies:
14 | - local-step1
15 |
--------------------------------------------------------------------------------
/helm-chart/templates/config.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.piper.workflowsConfig }}
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: piper-workflows-config
6 | labels:
7 | {{- include "piper.labels" . | nindent 4 }}
8 | data:
9 | {{- with .Values.piper.workflowsConfig }}
10 | {{- toYaml . | nindent 2 }}
11 | {{- end }}
12 | {{- end }}
13 |
--------------------------------------------------------------------------------
/examples/.workflows/triggers.yaml:
--------------------------------------------------------------------------------
1 | - events:
2 | - push
3 | - pull_request.synchronize
4 | branches: ["main"]
5 | onStart: ["main.yaml"]
6 | onExit: ["exit.yaml"]
7 | templates: ["templates.yaml"]
8 | config: "default"
9 |
10 | - events:
11 | - pull_request
12 | branches: ["*"]
13 | onStart: ["main.yaml"]
14 | onExit: ["exit.yaml"]
15 | templates: ["templates.yaml"]
--------------------------------------------------------------------------------
/pkg/common/types.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/rookout/piper/pkg/git_provider"
5 | )
6 |
7 | type WorkflowsBatch struct {
8 | OnStart []*git_provider.CommitFile
9 | OnExit []*git_provider.CommitFile
10 | Templates []*git_provider.CommitFile
11 | Parameters *git_provider.CommitFile
12 | Config *string
13 | Payload *git_provider.WebhookPayload
14 | }
15 |
--------------------------------------------------------------------------------
/helm-chart/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "piper.serviceAccountName" . }}
6 | labels:
7 | {{- include "piper.labels" . | nindent 4 }}
8 | {{- with .Values.serviceAccount.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | {{- end }}
13 |
--------------------------------------------------------------------------------
/scripts/init-argo-workflows.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -o errexit
3 |
4 | if [ -z "$(helm list -n workflows | grep argo-workflow)" ]; then
5 | # 7. Install argo workflows
6 | helm repo add argo https://argoproj.github.io/argo-helm
7 | helm upgrade --install argo-workflow argo/argo-workflows -n workflows --create-namespace -f workflows.values.yaml
8 | else
9 | echo "Workflows release exists, skipping installation"
10 | fi
11 |
--------------------------------------------------------------------------------
/pkg/event_handler/types.go:
--------------------------------------------------------------------------------
1 | package event_handler
2 |
3 | import (
4 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
5 | "golang.org/x/net/context"
6 | "k8s.io/apimachinery/pkg/watch"
7 | )
8 |
9 | type EventHandler interface {
10 | Handle(ctx context.Context, event *watch.Event) error
11 | }
12 |
13 | type EventNotifier interface {
14 | Notify(ctx *context.Context, workflow *v1alpha1.Workflow) error
15 | }
16 |
--------------------------------------------------------------------------------
/helm-chart/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "piper.fullname" . }}
5 | labels:
6 | {{- include "piper.labels" . | nindent 4 }}
7 | spec:
8 | type: {{ .Values.service.type }}
9 | ports:
10 | - port: {{ .Values.service.port }}
11 | targetPort: 8080
12 | protocol: TCP
13 | name: http
14 | selector:
15 | {{- include "piper.selectorLabels" . | nindent 4 }}
--------------------------------------------------------------------------------
/.github/workflows/lint-commit.yaml:
--------------------------------------------------------------------------------
1 | name: Lint Commit
2 | on: pull_request
3 | jobs:
4 | conventional:
5 | name: Conventional Commit Linter
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2
9 | - uses: actions/setup-node@v2
10 | - uses: taskmedia/action-conventional-commits@v1.1.8
11 | with:
12 | token: ${{ github.token }}
13 | types: "fix|feat|revert|ci|docs|chore"
14 |
--------------------------------------------------------------------------------
/workflows.values.yaml:
--------------------------------------------------------------------------------
1 | controller:
2 | workflowNamespaces:
3 | - workflows
4 | server:
5 | baseHref: /argo/
6 | serviceAccount:
7 | create: true
8 | extraArgs:
9 | - server
10 | - --auth-mode=server
11 | ingress:
12 | enabled: true
13 | annotations:
14 | nginx.ingress.kubernetes.io/rewrite-target: /$1
15 | nginx.ingress.kubernetes.io/backend-protocol: HTTP
16 | paths:
17 | - /argo/(.*)
18 | - /argo
--------------------------------------------------------------------------------
/.github/workflows/release-please.yaml:
--------------------------------------------------------------------------------
1 | name: Release Please
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | release-please:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: google-github-actions/release-please-action@v3
17 | with:
18 | release-type: helm
19 | package-name: piper
20 | token: ${{ secrets.GIT_TOKEN }}
--------------------------------------------------------------------------------
/helm-chart/templates/rookout-token.yaml:
--------------------------------------------------------------------------------
1 | {{- if and .Values.rookout.token (not .Values.rookout.existingSecret) }}
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: {{ template "rookout.secretName" . }}
6 | namespace: {{ .Release.Namespace }}
7 | labels:
8 | app.kubernetes.io/name: {{ .Chart.Name }}
9 | app.kubernetes.io/instance: {{ .Release.Name }}
10 | type: Opaque
11 | data:
12 | token: {{ .Values.rookout.token | b64enc | quote }}
13 | {{- end }}
14 |
--------------------------------------------------------------------------------
/helm-chart/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 | _lint.yaml
--------------------------------------------------------------------------------
/helm-chart/templates/git-token-secret.yaml:
--------------------------------------------------------------------------------
1 | {{- if and .Values.piper.gitProvider.token (not .Values.piper.gitProvider.existingSecret) }}
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: {{ template "piper.gitProvider.tokenSecretName" . }}
6 | namespace: {{ .Release.Namespace }}
7 | labels:
8 | app.kubernetes.io/name: {{ .Chart.Name }}
9 | app.kubernetes.io/instance: {{ .Release.Name }}
10 | type: Opaque
11 | data:
12 | token: {{ .Values.piper.gitProvider.token | b64enc | quote }}
13 | {{- end }}
14 |
--------------------------------------------------------------------------------
/pkg/server/main.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/rookout/piper/pkg/clients"
5 | "github.com/rookout/piper/pkg/conf"
6 | "golang.org/x/net/context"
7 | "log"
8 | )
9 |
10 | func Start(ctx context.Context, stop context.CancelFunc, cfg *conf.GlobalConfig, clients *clients.Clients) {
11 |
12 | srv := NewServer(cfg, clients)
13 | gracefulShutdownHandler := NewGracefulShutdown(ctx, stop)
14 | srv.Start()
15 |
16 | gracefulShutdownHandler.Shutdown(srv)
17 |
18 | log.Println("Server exiting")
19 | }
20 |
--------------------------------------------------------------------------------
/helm-chart/templates/argo-token-secret.yaml:
--------------------------------------------------------------------------------
1 | {{- if and .Values.piper.argoWorkflows.server.token (not .Values.piper.argoWorkflows.server.existingSecret) }}
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: {{ template "piper.argoWorkflows.tokenSecretName" . }}
6 | namespace: {{ .Release.Namespace }}
7 | labels:
8 | app.kubernetes.io/name: {{ .Chart.Name }}
9 | app.kubernetes.io/instance: {{ .Release.Name }}
10 | type: Opaque
11 | data:
12 | token: {{ .Values.piper.argoWorkflows.server.token | b64enc | quote }}
13 | {{- end }}
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/pkg/server/types.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/rookout/piper/pkg/clients"
6 | "github.com/rookout/piper/pkg/conf"
7 | "github.com/rookout/piper/pkg/webhook_creator"
8 | "net/http"
9 | )
10 |
11 | type Server struct {
12 | router *gin.Engine
13 | config *conf.GlobalConfig
14 | clients *clients.Clients
15 | webhookCreator *webhook_creator.WebhookCreatorImpl
16 | httpServer *http.Server
17 | }
18 |
19 | type Interface interface {
20 | startServer() *http.Server
21 | registerMiddlewares()
22 | getRoutes()
23 | Start() *http.Server
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/webhook_handler/types.go:
--------------------------------------------------------------------------------
1 | package webhook_handler
2 |
3 | import (
4 | "context"
5 | "github.com/rookout/piper/pkg/common"
6 | )
7 |
8 | type Trigger struct {
9 | Events *[]string `yaml:"events"`
10 | Branches *[]string `yaml:"branches"`
11 | OnStart *[]string `yaml:"onStart"`
12 | Templates *[]string `yaml:"templates"`
13 | OnExit *[]string `yaml:"onExit"`
14 | Config string `yaml:"config" default:"default"`
15 | }
16 |
17 | type WebhookHandler interface {
18 | RegisterTriggers(ctx *context.Context) error
19 | PrepareBatchForMatchingTriggers(ctx *context.Context) ([]*common.WorkflowsBatch, error)
20 | }
21 |
--------------------------------------------------------------------------------
/helm-chart/templates/webhook-secret.yaml:
--------------------------------------------------------------------------------
1 | {{- if not .Values.piper.gitProvider.webhook.existingSecret }}
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: {{ template "piper.gitProvider.webhook.secretName" . }}
6 | namespace: {{ .Release.Namespace }}
7 | labels:
8 | app.kubernetes.io/name: {{ .Chart.Name }}
9 | app.kubernetes.io/instance: {{ .Release.Name }}
10 | type: Opaque
11 | data:
12 | {{- if and .Values.piper.gitProvider.webhook.secret }}
13 | secret: {{ .Values.piper.gitProvider.webhook.secret | b64enc | quote }}
14 | {{- else }}
15 | secret: {{ randAlphaNum 30 | b64enc | quote }}
16 | {{- end }}
17 | {{- end }}
18 |
--------------------------------------------------------------------------------
/pkg/conf/rookout.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/kelseyhightower/envconfig"
7 | )
8 |
9 | type RookoutConfig struct {
10 | Token string `envconfig:"ROOKOUT_TOKEN" default:""`
11 | Labels string `envconfig:"ROOKOUT_LABELS" default:"service:piper"`
12 | RemoteOrigin string `envconfig:"ROOKOUT_REMOTE_ORIGIN" default:"https://github.com/Rookout/piper.git"`
13 | }
14 |
15 | func (cfg *RookoutConfig) RookoutConfLoad() error {
16 | err := envconfig.Process("", cfg)
17 | if err != nil {
18 | return fmt.Errorf("failed to load the Rookout configuration, error: %v", err)
19 | }
20 |
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 | "github.com/kelseyhightower/envconfig"
6 | )
7 |
8 | type GlobalConfig struct {
9 | GitProviderConfig
10 | WorkflowServerConfig
11 | RookoutConfig
12 | WorkflowsConfig
13 | }
14 |
15 | func (cfg *GlobalConfig) Load() error {
16 | err := envconfig.Process("", cfg)
17 | if err != nil {
18 | return fmt.Errorf("failed to load the configuration, error: %v", err)
19 | }
20 |
21 | return nil
22 | }
23 |
24 | func LoadConfig() (*GlobalConfig, error) {
25 | cfg := new(GlobalConfig)
26 |
27 | err := cfg.Load()
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | return cfg, nil
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/git_provider/main.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/rookout/piper/pkg/conf"
6 | )
7 |
8 | func NewGitProviderClient(cfg *conf.GlobalConfig) (Client, error) {
9 |
10 | switch cfg.GitProviderConfig.Provider {
11 | case "github":
12 | gitClient, err := NewGithubClient(cfg)
13 | if err != nil {
14 | return nil, err
15 | }
16 | return gitClient, nil
17 | case "bitbucket":
18 | gitClient, err := NewBitbucketServerClient(cfg)
19 | if err != nil {
20 | return nil, err
21 | }
22 | return gitClient, nil
23 | }
24 |
25 | return nil, fmt.Errorf("didn't find matching git provider %s", cfg.GitProviderConfig.Provider)
26 | }
27 |
--------------------------------------------------------------------------------
/examples/template.values.dev.yaml:
--------------------------------------------------------------------------------
1 | piper:
2 | gitProvider:
3 | name: github
4 | token: "GIT_TOKEN"
5 | organization:
6 | name: "ORG_NAME"
7 | webhook:
8 | url: https://NGROK_ADDRESS/piper/webhook
9 | orgLevel: false
10 | repoList: ["REPO_NAME"]
11 | argoWorkflows:
12 | server:
13 | namespace: "workflows"
14 | address: "ARGO_ADDRESS"
15 | token: "ARGO_TOKEN"
16 | image:
17 | name: piper
18 | repository: localhost:5001
19 | pullPolicy: Always
20 | tag: latest
21 | ingress:
22 | enabled: true
23 | annotations:
24 | nginx.ingress.kubernetes.io/rewrite-target: /$2
25 | hosts:
26 | - paths:
27 | - path: /piper(/|$)(.*)
28 | pathType: ImplementationSpecific
--------------------------------------------------------------------------------
/scripts/init-nginx.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -o errexit
3 |
4 | if [ -z "$(kubectl get pods --all-namespaces | grep ingress-nginx-controller)" ]; then
5 | # 6. Deploy of nginx ingress controller to the cluster
6 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml && \
7 | kubectl wait --namespace ingress-nginx \
8 | --for=condition=complete job/ingress-nginx-admission-create \
9 | --timeout=180s && \
10 | kubectl wait --namespace ingress-nginx \
11 | --for=condition=ready pod \
12 | --selector=app.kubernetes.io/component=controller \
13 | --timeout=360s
14 | else
15 | echo "Nginx already exists, skipping installation"
16 | fi
17 |
--------------------------------------------------------------------------------
/pkg/conf/workflow_server.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 | "github.com/kelseyhightower/envconfig"
6 | )
7 |
8 | type WorkflowServerConfig struct {
9 | ArgoToken string `envconfig:"ARGO_WORKFLOWS_TOKEN" required:"false"`
10 | ArgoAddress string `envconfig:"ARGO_WORKFLOWS_ADDRESS" required:"false"`
11 | CreateCRD bool `envconfig:"ARGO_WORKFLOWS_CREATE_CRD" default:"true"`
12 | Namespace string `envconfig:"ARGO_WORKFLOWS_NAMESPACE" default:"default"`
13 | KubeConfig string `envconfig:"KUBE_CONFIG" default:""`
14 | }
15 |
16 | func (cfg *WorkflowServerConfig) ArgoConfLoad() error {
17 | err := envconfig.Process("", cfg)
18 | if err != nil {
19 | return fmt.Errorf("failed to load the Argo configuration, error: %v", err)
20 | }
21 |
22 | return nil
23 | }
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.20-alpine3.16 as builder
2 |
3 | WORKDIR /piper
4 |
5 | RUN apk update && apk add --no-cache \
6 | git \
7 | make \
8 | wget \
9 | curl \
10 | gcc \
11 | bash \
12 | ca-certificates \
13 | musl-dev \
14 | zlib-static \
15 | build-base
16 |
17 | COPY go.mod .
18 | COPY go.sum .
19 | RUN --mount=type=cache,target=/go/pkg/mod go mod download
20 |
21 | COPY . .
22 |
23 | RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go mod tidy
24 |
25 | RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go build -gcflags='all=-N -l' -tags=alpine -buildvcs=false -trimpath ./cmd/piper
26 |
27 |
28 | FROM alpine:3.16 as piper-release
29 |
30 | ENV GIN_MODE=release
31 |
32 | USER 1001
33 |
34 | COPY .git /.git
35 |
36 | COPY --chown=1001 --from=builder /piper/piper /bin
37 |
38 | ENTRYPOINT [ "piper" ]
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 |
4 |
5 |
6 |
7 | Welcome to Piper!
8 |
9 | Piper is an open source project that aimed at providing multibranch pipeline functionality to Argo Workflows, allows users to create distinct Workflows based on Git branches. Supports GitHub and Bitbucket.
10 |
11 | ## General explanation
12 |
13 |
14 |
15 |
16 |
17 | To achieve multibranch pipeline functionality Piper will do the hard works for us.
18 | At initialization, it will load all configuration and create a webhook in repository or organization scope.
19 | Then each branch that have `.workflows` folder will create a Workflow CRD out of the files in this folder.
20 |
21 | 
--------------------------------------------------------------------------------
/pkg/server/routes/healthz.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/rookout/piper/pkg/conf"
5 | "github.com/rookout/piper/pkg/webhook_creator"
6 | "golang.org/x/net/context"
7 | "log"
8 | "net/http"
9 | "time"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | func AddHealthRoutes(rg *gin.RouterGroup, wc *webhook_creator.WebhookCreatorImpl, cfg *conf.GlobalConfig) {
15 | health := rg.Group("/healthz")
16 |
17 | health.GET("", func(c *gin.Context) {
18 | if cfg.GitProviderConfig.FullHealthCheck {
19 | ctx := c.Copy().Request.Context()
20 | ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
21 | defer cancel()
22 | err := wc.RunDiagnosis(&ctx2)
23 | if err != nil {
24 | log.Printf("error from healthz endpoint:%s\n", err)
25 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
26 | return
27 | }
28 | }
29 | c.JSON(http.StatusOK, "healthy")
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/docs/configuration/health_check.md:
--------------------------------------------------------------------------------
1 | ## Health Check
2 |
3 | Health check executed every 1 minute as configured in the helm chart under `livenessProbe`, and triggered by `/healthz` endpoint:
4 | ```yaml
5 | livenessProbe:
6 | httpGet:
7 | path: /healthz
8 | port: 8080
9 | scheme: HTTP
10 | initialDelaySeconds: 10
11 | timeoutSeconds: 10
12 | periodSeconds: 60
13 | successThreshold: 1
14 | failureThreshold: 4
15 | ```
16 |
17 | The mechanism for checking the health of Piper is:
18 |
19 | 1. Piper set health status of all webhooks to not-healthy.
20 |
21 | 2. Piper requests ping from all the webhooks configured.
22 |
23 | 3. Git Provider send ping to `/webhook` endpoint, this will set the health status to `healthy` with timeout of 5 seconds.
24 |
25 | 4. Piper check the status of all webhooks configured.
26 |
27 | Therefore, the criteria for health checking are:
28 | 1. The registered webhook exists.
29 | 2. The webhook send a ping in 5 seconds.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/helm-chart/templates/hpa.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.autoscaling.enabled }}
2 | apiVersion: autoscaling/v2beta1
3 | kind: HorizontalPodAutoscaler
4 | metadata:
5 | name: {{ include "piper.fullname" . }}
6 | labels:
7 | {{- include "piper.labels" . | nindent 4 }}
8 | spec:
9 | scaleTargetRef:
10 | apiVersion: apps/v1
11 | kind: Deployment
12 | name: {{ include "piper.fullname" . }}
13 | minReplicas: {{ .Values.autoscaling.minReplicas }}
14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15 | metrics:
16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
17 | - type: Resource
18 | resource:
19 | name: cpu
20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
21 | {{- end }}
22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
23 | - type: Resource
24 | resource:
25 | name: memory
26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
27 | {{- end }}
28 | {{- end }}
29 |
--------------------------------------------------------------------------------
/examples/config.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: piper-workflows-config
5 | data:
6 | default: |
7 | spec:
8 | volumes:
9 | - name: shared-volume
10 | emptyDir: { }
11 | serviceAccountName: argo-wf
12 | activeDeadlineSeconds: 7200 # (seconds) == 2 hours
13 | ttlStrategy:
14 | secondsAfterCompletion: 28800 # (seconds) == 8 hours
15 | podGC:
16 | strategy: OnPodSuccess
17 | archiveLogs: true
18 | artifactRepositoryRef:
19 | configMap: artifact-repositories
20 | nodeSelector:
21 | node_pool: workflows
22 | tolerations:
23 | - effect: NoSchedule
24 | key: node_pool
25 | operator: Equal
26 | value: workflows
27 | onExit: # optinal, will be overwritten if specifc in .wokrflows/exit.yaml.
28 | - name: github-status
29 | template: exit-handler
30 | arguments:
31 | parameters:
32 | - name: param1
33 | value: "{{ workflow.labels.repo }}"
--------------------------------------------------------------------------------
/docs/usage/workflows_config.md:
--------------------------------------------------------------------------------
1 | ## Workflow Configuration
2 |
3 | Piper can inject configuration for Workflows that Piper creates.
4 |
5 | `default` config used as a convention for all Workflows that piper will create, even if not explicitly mentioned in triggers.yaml file.
6 |
7 | ### ConfigMap
8 | Piper will mount a configMap when helm used.
9 | `piper.workflowsConfig` variable in helm chart, will create a configMap that hold set of configuration for Piper.
10 | Here is an [examples](https://github.com/Rookout/piper/tree/main/examples/config.yaml) of such configuration.
11 |
12 | ### Spec
13 | This will be injected to Workflow spec field. can hold all configuration of the Workflow.
14 | > :warning: Please notice that the fields `entrypoint` and `onExit` should not exist in spec. both of them are managed fields.
15 |
16 | ### onExit
17 | This is the exit handler for each of the Workflows create by piper.
18 | It configures a DAG that will be executed when the workflow ends.
19 | You can provide the templates to it us in the following [Examples](https://github.com/Rookout/piper/tree/main/examples/config.yaml).
--------------------------------------------------------------------------------
/helm-chart/templates/security.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRole
3 | metadata:
4 | name: {{ include "piper.fullname" . }}
5 | rules:
6 | - apiGroups:
7 | - ""
8 | resources:
9 | - events
10 | verbs:
11 | - list
12 | - watch
13 | - create
14 | - patch
15 | - apiGroups:
16 | - argoproj.io
17 | resources:
18 | - workflows
19 | - workflows/finalizers
20 | - workflowtasksets
21 | - workflowtasksets/finalizers
22 | - workflowartifactgctasks
23 | verbs:
24 | - get
25 | - list
26 | - watch
27 | - update
28 | - patch
29 | - delete
30 | - create
31 | ---
32 | apiVersion: rbac.authorization.k8s.io/v1
33 | kind: ClusterRoleBinding
34 | metadata:
35 | name: {{ include "piper.fullname" . }}
36 | subjects:
37 | - kind: ServiceAccount
38 | name: {{ include "piper.fullname" . }}
39 | namespace: {{ .Release.Namespace | quote }}
40 | roleRef:
41 | apiGroup: rbac.authorization.k8s.io
42 | kind: ClusterRole
43 | name: {{ include "piper.fullname" . }}
44 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Pull Request
2 |
3 | ### Description
4 |
5 | Please provide a brief description of the changes made in this pull request.
6 |
7 | ### Related Issue(s)
8 |
9 | If this pull request addresses or relates to any open issues, please mention them here using the syntax `Fixes #` or `Resolves #`.
10 |
11 | ### Checklist
12 |
13 | Before submitting this pull request, please ensure that you have completed the following tasks:
14 |
15 | - [ ] Reviewed the [Contributing Guidelines](../docs/CONTRIBUTING.md) for the Piper project.
16 | - [ ] Ensured that your changes follow the [coding guidelines and style](../docs/CONTRIBUTING.md#coding-guidelines) of the project.
17 | - [ ] Run the tests locally and made sure they pass.
18 | - [ ] Updated the relevant documentation, if applicable.
19 |
20 | ### Testing Instructions
21 |
22 | Please provide clear instructions on how to test and verify the changes made in this pull request.
23 |
24 | ### Additional Information
25 |
26 | Add any additional information or context that would be helpful in understanding and reviewing this pull request.
27 |
--------------------------------------------------------------------------------
/pkg/workflow_handler/types.go:
--------------------------------------------------------------------------------
1 | package workflow_handler
2 |
3 | import (
4 | "context"
5 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
6 | "github.com/rookout/piper/pkg/common"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | "k8s.io/apimachinery/pkg/watch"
9 | )
10 |
11 | type WorkflowsClient interface {
12 | ConstructTemplates(workflowsBatch *common.WorkflowsBatch, configName string) ([]v1alpha1.Template, error)
13 | ConstructSpec(templates []v1alpha1.Template, params []v1alpha1.Parameter, configName string) (*v1alpha1.WorkflowSpec, error)
14 | CreateWorkflow(spec *v1alpha1.WorkflowSpec, workflowsBatch *common.WorkflowsBatch) (*v1alpha1.Workflow, error)
15 | SelectConfig(workflowsBatch *common.WorkflowsBatch) (string, error)
16 | Lint(wf *v1alpha1.Workflow) error
17 | Submit(ctx *context.Context, wf *v1alpha1.Workflow) error
18 | HandleWorkflowBatch(ctx *context.Context, workflowsBatch *common.WorkflowsBatch) error
19 | Watch(ctx *context.Context, labelSelector *metav1.LabelSelector) (watch.Interface, error)
20 | UpdatePiperWorkflowLabel(ctx *context.Context, workflowName string, label string, value string) error
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/conf/workflows_config.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 |
7 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
8 | "github.com/rookout/piper/pkg/utils"
9 | )
10 |
11 | type WorkflowsConfig struct {
12 | Configs map[string]*ConfigInstance
13 | }
14 |
15 | type ConfigInstance struct {
16 | Spec v1alpha1.WorkflowSpec `yaml:"spec"`
17 | OnExit []v1alpha1.DAGTask `yaml:"onExit"`
18 | }
19 |
20 | func (wfc *WorkflowsConfig) WorkflowsSpecLoad(configPath string) error {
21 | var jsonBytes []byte
22 | wfc.Configs = make(map[string]*ConfigInstance)
23 |
24 | configs, err := utils.GetFilesData(configPath)
25 | if len(configs) == 0 {
26 | log.Printf("No config files to load at %s", configPath)
27 | return nil
28 | }
29 | if err != nil {
30 | return err
31 | }
32 |
33 | for key, config := range configs {
34 | tmp := new(ConfigInstance)
35 | jsonBytes, err = utils.ConvertYAMLToJSON(config)
36 | if err != nil {
37 | return err
38 | }
39 | err = json.Unmarshal(jsonBytes, &tmp)
40 | if err != nil {
41 | return err
42 | }
43 | wfc.Configs[key] = tmp
44 | }
45 |
46 | return nil
47 | }
48 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yaml:
--------------------------------------------------------------------------------
1 | name: docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - 'docs/**'
8 | - '.github/workflows/docs.yaml'
9 | - 'mkdocs.yml'
10 |
11 | permissions:
12 | contents: write
13 | jobs:
14 | publish:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout main
18 | uses: actions/checkout@v2
19 | - name: Install mkdocs
20 | run: |
21 | pip install mkdocs-material
22 | pip install mkdocs-video
23 | - name: Generate docs artifacts
24 | run: mkdocs build -s -d /tmp/docs
25 | - uses: actions/checkout@v2
26 | with:
27 | ref: gh-pages
28 | path: gh-pages
29 | - name: Publish docs artifacts to gh-pages
30 | run: |
31 | cd gh-pages
32 | shopt -s extglob
33 | rm -rf !(index.yaml|LICENSE|*.tgz)
34 | cp -R /tmp/docs/** .
35 | git config --local user.email "action@github.com"
36 | git config --local user.name "GitHub Action"
37 | git add -A
38 | git commit -m "Publish docs from $GITHUB_SHA"
39 | git push https://github.com/$GITHUB_REPOSITORY.git gh-pages
--------------------------------------------------------------------------------
/pkg/server/shutdown.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "golang.org/x/net/context"
5 | "log"
6 | "time"
7 | )
8 |
9 | type GracefulShutdown struct {
10 | ctx context.Context
11 | stop context.CancelFunc
12 | }
13 |
14 | func NewGracefulShutdown(ctx context.Context, stop context.CancelFunc) *GracefulShutdown {
15 | return &GracefulShutdown{
16 | ctx: ctx,
17 | stop: stop,
18 | }
19 | }
20 |
21 | func (s *GracefulShutdown) StopServices(ctx *context.Context, server *Server) {
22 | server.webhookCreator.Stop(ctx)
23 | }
24 |
25 | func (s *GracefulShutdown) Shutdown(server *Server) {
26 | // Listen for the interrupt signal.
27 | <-s.ctx.Done()
28 |
29 | // Restore default behavior on the interrupt signal and notify user of shutdown.
30 | s.stop()
31 |
32 | log.Println("shutting down gracefully...")
33 | // The context is used to inform the server it has 10 seconds to finish
34 | // the request it is currently handling
35 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
36 | defer cancel()
37 |
38 | s.StopServices(&ctx, server)
39 |
40 | err := server.httpServer.Shutdown(ctx)
41 | if err != nil {
42 | log.Fatal("Server forced to shutdown: ", err)
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/event_handler/main.go:
--------------------------------------------------------------------------------
1 | package event_handler
2 |
3 | import (
4 | "context"
5 | "github.com/rookout/piper/pkg/clients"
6 | "github.com/rookout/piper/pkg/conf"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | "log"
9 | )
10 |
11 | func Start(ctx context.Context, stop context.CancelFunc, cfg *conf.GlobalConfig, clients *clients.Clients) {
12 | labelSelector := &metav1.LabelSelector{
13 | MatchExpressions: []metav1.LabelSelectorRequirement{
14 | {Key: "piper.rookout.com/notified",
15 | Operator: metav1.LabelSelectorOpExists},
16 | },
17 | }
18 | watcher, err := clients.Workflows.Watch(&ctx, labelSelector)
19 | if err != nil {
20 | log.Printf("[event handler] Failed to watch workflow error:%s", err)
21 | return
22 | }
23 |
24 | notifier := NewGithubEventNotifier(cfg, clients)
25 | handler := &workflowEventHandler{
26 | Clients: clients,
27 | Notifier: notifier,
28 | }
29 | go func() {
30 | for event := range watcher.ResultChan() {
31 | err2 := handler.Handle(ctx, &event)
32 | if err2 != nil {
33 | log.Printf("[event handler] failed to Handle workflow event %s", err2) // ERROR
34 | }
35 | }
36 | log.Print("[event handler] stopped work, closing watcher")
37 | watcher.Stop()
38 | stop()
39 | }()
40 | }
41 |
--------------------------------------------------------------------------------
/docs/usage/global_variables.md:
--------------------------------------------------------------------------------
1 | ## Global variables
2 |
3 | Piper will automatically add Workflow scope parameters that can be referenced from any template.
4 | The parameters taken from webhook metadata, and will be populated respectively to GitProvider and event that triggered the workflow.
5 |
6 | 1. `{{ workflow.parameters.event }}` the event that triggered the workflow.
7 |
8 | 2. `{{ workflow.parameters.action }}` the action that triggered the workflow.
9 |
10 | 3. `{{ workflow.parameters.dest_branch }}` the destination branch for pull request.
11 |
12 | 4. `{{ workflow.parameters.commit }}` the commit that triggered the workflow.
13 |
14 | 5. `{{ workflow.parameters.repo }}` repository name that triggered the workflow.
15 |
16 | 6. `{{ workflow.parameters.user }}` the username that triggered the workflow.
17 |
18 | 7. `{{ workflow.parameters.user_email }}` the user's email that triggered the workflow.
19 |
20 | 8. `{{ workflow.parameters.pull_request_url }}` the url of the pull request that triggered the workflow.
21 |
22 | 9. `{{workflow.parameters.pull_request_title }}` the tile of the pull request that triggered the workflow.
23 |
24 | 10. `{{workflow.parameters.pull_request_labels }}` comma seperated labels of the pull request that triggered the workflow.
25 |
--------------------------------------------------------------------------------
/pkg/conf/git_provider.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/kelseyhightower/envconfig"
7 | )
8 |
9 | type GitProviderConfig struct {
10 | Provider string `envconfig:"GIT_PROVIDER" required:"true"`
11 | Token string `envconfig:"GIT_TOKEN" required:"true"`
12 | OrgName string `envconfig:"GIT_ORG_NAME" required:"true"`
13 | OrgLevelWebhook bool `envconfig:"GIT_ORG_LEVEL_WEBHOOK" default:"false" required:"false"`
14 | RepoList string `envconfig:"GIT_WEBHOOK_REPO_LIST" required:"false"`
15 | WebhookURL string `envconfig:"GIT_WEBHOOK_URL" required:"false"`
16 | WebhookSecret string `envconfig:"GIT_WEBHOOK_SECRET" required:"false"`
17 | WebhookAutoCleanup bool `envconfig:"GIT_WEBHOOK_AUTO_CLEANUP" default:"false" required:"false"`
18 | EnforceOrgBelonging bool `envconfig:"GIT_ENFORCE_ORG_BELONGING" default:"false" required:"false"`
19 | OrgID int64
20 | FullHealthCheck bool `envconfig:"GIT_FULL_HEALTH_CHECK" default:"false" required:"false"`
21 | }
22 |
23 | func (cfg *GitProviderConfig) GitConfLoad() error {
24 | err := envconfig.Process("", cfg)
25 | if err != nil {
26 | return fmt.Errorf("failed to load the Git provider configuration, error: %v", err)
27 | }
28 |
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/lint-pr.yaml:
--------------------------------------------------------------------------------
1 | name: Check PR title
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - reopened
8 | - edited
9 | - synchronize
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | lint:
16 | permissions:
17 | pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
18 | statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
19 | name: Validate PR title
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: amannn/action-semantic-pull-request@v5
23 | with:
24 | # Configure which types are allowed (newline delimited).
25 | # Default: https://github.com/commitizen/conventional-commit-types
26 | types: |
27 | feat
28 | fix
29 | docs
30 | chore
31 | revert
32 | ci
33 | # Configure which scopes are allowed (newline delimited).
34 | scopes: |
35 | deps
36 | core
37 | RK-\d+
38 | # Configure that a scope must always be provided.
39 | requireScope: false
40 | ignoreLabels: |
41 | autorelease: pending
42 | env:
43 | GITHUB_TOKEN: ${{ github.token }}
44 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Piper - Multibranch Pipeline for ArgoWorkflows
2 | site_description: 'Piper project for multibranch pipeline in Argo Workflows'
3 | site_author: 'George Dozoretz'
4 | docs_dir: docs/
5 | repo_url: https://github.com/rookout/piper
6 | repo_name: rookout/piper
7 | theme:
8 | name: material
9 | icon:
10 | repo: fontawesome/brands/github
11 | palette:
12 | - scheme: default
13 | toggle:
14 | icon: material/weather-night
15 | name: Switch to dark mode
16 | - scheme: slate
17 | toggle:
18 | icon: material/weather-sunny
19 | name: Switch to light mode
20 | features:
21 | - content.code.annotate
22 | plugins:
23 | - mkdocs-video:
24 | is_video: True
25 | video_loop: True
26 | video_muted: True
27 | video_autoplay: True
28 | markdown_extensions:
29 | - pymdownx.highlight:
30 | anchor_linenums: true
31 | - pymdownx.inlinehilite
32 | - pymdownx.snippets
33 | - pymdownx.superfences
34 |
35 | nav:
36 | - Introduction: index.md
37 | - Getting Started: getting_started/installation.md
38 | - Configuration:
39 | - configuration/environment_variables.md
40 | - configuration/health_check.md
41 | - Use piper:
42 | - usage/workflows_folder.md
43 | - usage/global_variables.md
44 | - usage/workflows_config.md
45 | - Developers: CONTRIBUTING.md
--------------------------------------------------------------------------------
/pkg/utils/os.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | func GetFilesData(directory string) (map[string][]byte, error) {
9 | fileData := make(map[string][]byte)
10 | var data []byte
11 |
12 | path, dirList, err := GetFilesInLinkDirectory(directory)
13 | if err != nil {
14 | return nil, err
15 | }
16 | for _, dir := range dirList {
17 | data, err = GetFileData(*path + "/" + dir)
18 | if err != nil {
19 | return nil, err
20 | }
21 | fileData[dir] = data
22 | }
23 |
24 | return fileData, nil
25 | }
26 |
27 | func GetFileData(filePath string) ([]byte, error) {
28 | data, err := os.ReadFile(filePath)
29 | if err != nil {
30 | return nil, err
31 | }
32 | return data, nil
33 | }
34 |
35 | func GetFilesInLinkDirectory(linkPath string) (*string, []string, error) {
36 | realPath, err := filepath.EvalSymlinks(linkPath)
37 | if err != nil {
38 | return nil, nil, err
39 | }
40 |
41 | var fileNames []string
42 | err = filepath.WalkDir(realPath, func(path string, d os.DirEntry, err error) error {
43 | if err != nil {
44 | return err
45 | }
46 | if !d.IsDir() {
47 | info, err := d.Info()
48 | if err != nil {
49 | return err
50 | }
51 | if info.Mode()&os.ModeSymlink != 0 {
52 | return nil // Skip symbolic links
53 | }
54 | relPath, err := filepath.Rel(realPath, path)
55 | if err != nil {
56 | return err
57 | }
58 | fileNames = append(fileNames, relPath)
59 | }
60 | return nil
61 | })
62 |
63 | if err != nil {
64 | return nil, nil, err
65 | }
66 |
67 | return &realPath, fileNames, nil
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/rookout/piper/pkg/clients"
6 | "github.com/rookout/piper/pkg/conf"
7 | "github.com/rookout/piper/pkg/server/routes"
8 | "github.com/rookout/piper/pkg/webhook_creator"
9 | "log"
10 | "net/http"
11 | )
12 |
13 | func NewServer(config *conf.GlobalConfig, clients *clients.Clients) *Server {
14 | srv := &Server{
15 | router: gin.New(),
16 | config: config,
17 | clients: clients,
18 | webhookCreator: webhook_creator.NewWebhookCreator(config, clients),
19 | }
20 |
21 | return srv
22 | }
23 |
24 | func (s *Server) startServer() *http.Server {
25 | srv := &http.Server{
26 | Addr: ":8080",
27 | Handler: s.router,
28 | }
29 |
30 | go func() {
31 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
32 | log.Fatalf("listen: %s\n", err)
33 | }
34 | }()
35 |
36 | return srv
37 | }
38 |
39 | func (s *Server) registerMiddlewares() {
40 | s.router.Use(
41 | gin.LoggerWithConfig(gin.LoggerConfig{
42 | SkipPaths: []string{"/healthz", "/readyz"},
43 | }),
44 | gin.Recovery(),
45 | )
46 |
47 | }
48 |
49 | func (s *Server) getRoutes() {
50 | v1 := s.router.Group("/")
51 | routes.AddReadyRoutes(v1)
52 | routes.AddHealthRoutes(v1, s.webhookCreator, s.config)
53 | routes.AddWebhookRoutes(s.config, s.clients, v1, s.webhookCreator)
54 | }
55 |
56 | func (s *Server) startServices() {
57 | s.webhookCreator.Start()
58 | }
59 |
60 | func (s *Server) Start() {
61 |
62 | s.registerMiddlewares()
63 |
64 | s.getRoutes()
65 |
66 | s.httpServer = s.startServer()
67 |
68 | s.startServices()
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/sh
2 | CLUSTER_DEPLOYED := $(shell kind get clusters -q | grep piper)
3 |
4 | .PHONY: ngrok
5 | ngrok:
6 | ngrok http 80
7 |
8 | .PHONY: local-build
9 | local-build:
10 | DOCKER_BUILDKIT=1 docker build -t localhost:5001/piper:latest .
11 |
12 | .PHONY: local-push
13 | local-push:
14 | docker push localhost:5001/piper:latest
15 |
16 | .PHONY: init-kind
17 | init-kind:
18 | ifndef CLUSTER_DEPLOYED
19 | sh ./scripts/init-kind.sh
20 | else
21 | $(info Kind piper cluster exists, skipping cluster installation)
22 | endif
23 | kubectl config set-context kind-piper
24 |
25 | .PHONY: init-nginx
26 | init-nginx: init-kind
27 | sh ./scripts/init-nginx.sh
28 |
29 | .PHONY: init-argo-workflows
30 | init-argo-workflows: init-kind
31 | sh ./scripts/init-argo-workflows.sh
32 |
33 | .PHONY: init-piper
34 | init-piper: init-kind local-build
35 | sh ./scripts/init-piper.sh
36 |
37 | .PHONY: deploy
38 | deploy: init-kind init-nginx init-argo-workflows local-build local-push init-piper
39 |
40 | .PHONY: restart
41 | restart: local-build
42 | docker push localhost:5001/piper:latest
43 | kubectl rollout restart deployment piper
44 |
45 | .PHONY: clean
46 | clean:
47 | kind delete cluster --name piper
48 | docker stop kind-registry && docker rm kind-registry
49 |
50 | .PHONY: helm
51 | helm:
52 | helm lint ./helm-chart
53 | helm template ./helm-chart --debug > _lint.yaml
54 | helm-docs
55 |
56 | .PHONY: test
57 | test:
58 | go test -short ./pkg/...
59 |
60 | $(GOPATH)/bin/golangci-lint:
61 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b `go env GOPATH`/bin v1.52.2
62 |
63 | .PHONY: lint
64 | lint: $(GOPATH)/bin/golangci-lint
65 | $(GOPATH)/bin/golangci-lint run --fix --verbose
--------------------------------------------------------------------------------
/pkg/git_provider/types.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | )
7 |
8 | type HookWithStatus struct {
9 | HookID int64
10 | Uuid string
11 | HealthStatus bool
12 | RepoName *string
13 | }
14 |
15 | type CommitFile struct {
16 | Path *string `json:"path"`
17 | Content *string `json:"content"`
18 | }
19 |
20 | type WebhookPayload struct {
21 | Event string `json:"event"`
22 | Action string `json:"action"`
23 | Repo string `json:"repoName"`
24 | Branch string `json:"branch"`
25 | Commit string `json:"commit"`
26 | User string `json:"user"`
27 | UserEmail string `json:"user_email"`
28 | PullRequestURL string `json:"pull_request_url"`
29 | PullRequestTitle string `json:"pull_request_title"`
30 | DestBranch string `json:"dest_branch"`
31 | Labels []string `json:"labels"`
32 | HookID int64 `json:"hookID"`
33 | OwnerID int64 `json:"ownerID"`
34 | }
35 |
36 | type Client interface {
37 | ListFiles(ctx *context.Context, repo string, branch string, path string) ([]string, error)
38 | GetFile(ctx *context.Context, repo string, branch string, path string) (*CommitFile, error)
39 | GetFiles(ctx *context.Context, repo string, branch string, paths []string) ([]*CommitFile, error)
40 | SetWebhook(ctx *context.Context, repo *string) (*HookWithStatus, error)
41 | UnsetWebhook(ctx *context.Context, hook *HookWithStatus) error
42 | HandlePayload(ctx *context.Context, request *http.Request, secret []byte) (*WebhookPayload, error)
43 | SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error
44 | PingHook(ctx *context.Context, hook *HookWithStatus) error
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release Workflow
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 | - edited
8 |
9 | jobs:
10 | piper-image:
11 | name: piper-image
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: docker/setup-qemu-action@v2
16 | - uses: docker/setup-buildx-action@v2
17 | - name: Login to Docker Hub
18 | uses: docker/login-action@v2
19 | with:
20 | username: ${{ secrets.DOCKERHUB_USERNAME }}
21 | password: ${{ secrets.DOCKERHUB_TOKEN }}
22 | - name: Docker meta
23 | id: meta
24 | uses: docker/metadata-action@v4
25 | with:
26 | images: rookout/piper
27 | - name: Build and export
28 | uses: docker/build-push-action@v4
29 | with:
30 | context: .
31 | platforms: linux/amd64,linux/arm64
32 | push: true
33 | tags: rookout/piper:${{ github.ref_name }},latest
34 | labels: ${{ steps.meta.outputs.labels }}
35 | cache-from: type=gha
36 | cache-to: type=gha,mode=max
37 | helm:
38 | name: helm
39 | runs-on: ubuntu-latest
40 | steps:
41 | - uses: actions/checkout@v3
42 | - name: Install Helm
43 | uses: azure/setup-helm@v3
44 | - name: Install Helm Docs
45 | uses: envoy/install-helm-docs@v1.0.0
46 | with:
47 | version: 1.11.0
48 | - name: Helm lint and template
49 | run: |
50 | make helm
51 | - name: Publish Helm chart
52 | uses: stefanprodan/helm-gh-pages@master
53 | with:
54 | chart_version: ${{ github.ref_name }}
55 | app_version: ${{ github.ref_name }}
56 | token: ${{ secrets.GIT_TOKEN }}
57 | charts_dir: .
--------------------------------------------------------------------------------
/pkg/event_handler/workflow_event_handler.go:
--------------------------------------------------------------------------------
1 | package event_handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
6 | "github.com/rookout/piper/pkg/clients"
7 | "golang.org/x/net/context"
8 | "k8s.io/apimachinery/pkg/watch"
9 | "log"
10 | )
11 |
12 | type workflowEventHandler struct {
13 | Clients *clients.Clients
14 | Notifier EventNotifier
15 | }
16 |
17 | func (weh *workflowEventHandler) Handle(ctx context.Context, event *watch.Event) error {
18 | workflow, ok := event.Object.(*v1alpha1.Workflow)
19 | if !ok {
20 | return fmt.Errorf(
21 | "event object is not a Workflow object, got: %v\n",
22 | event.DeepCopy().Object,
23 | )
24 | }
25 |
26 | currentPiperNotifyLabelStatus, ok := workflow.GetLabels()["piper.rookout.com/notified"]
27 | if !ok {
28 | return fmt.Errorf(
29 | "workflow %s missing piper.rookout.com/notified label\n",
30 | workflow.GetName(),
31 | )
32 | }
33 |
34 | if currentPiperNotifyLabelStatus == string(workflow.Status.Phase) {
35 | log.Printf(
36 | "workflow %s already informed for %s status. skiping... \n",
37 | workflow.GetName(),
38 | workflow.Status.Phase,
39 | ) //INFO
40 | return nil
41 | }
42 |
43 | err := weh.Notifier.Notify(&ctx, workflow)
44 | if err != nil {
45 | return fmt.Errorf("failed to Notify workflow to git provider, error:%s\n", err)
46 | }
47 |
48 | err = weh.Clients.Workflows.UpdatePiperWorkflowLabel(&ctx, workflow.GetName(), "notified", string(workflow.Status.Phase))
49 | if err != nil {
50 | return fmt.Errorf("error in workflow %s status patch: %s", workflow.GetName(), err)
51 | }
52 | log.Printf(
53 | "[event handler] done with event of type: %s for worklfow: %s phase: %s message: %s\n",
54 | event.Type,
55 | workflow.GetName(),
56 | workflow.Status.Phase,
57 | workflow.Status.Message) //INFO
58 |
59 | return nil
60 | }
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .history/
3 | .vscode
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | .hypothesis/
51 | .pytest_cache/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # Environments
88 | dev.env
89 | *.env
90 | .venv
91 | env/
92 | venv/
93 | ENV/
94 | env.bak/
95 | venv.bak/
96 |
97 | # Spyder project settings
98 | .spyderproject
99 | .spyproject
100 |
101 | # Rope project settings
102 | .ropeproject
103 |
104 | # mkdocs documentation
105 | /site
106 |
107 | # mypy
108 | .mypy_cache/
109 | *.iml
110 |
111 | _lint.yaml
112 | values.dev.yaml
--------------------------------------------------------------------------------
/cmd/piper/piper.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | rookout "github.com/Rookout/GoSDK"
5 | "github.com/rookout/piper/pkg/clients"
6 | "github.com/rookout/piper/pkg/conf"
7 | "github.com/rookout/piper/pkg/event_handler"
8 | "github.com/rookout/piper/pkg/git_provider"
9 | "github.com/rookout/piper/pkg/server"
10 | "github.com/rookout/piper/pkg/utils"
11 | workflowHandler "github.com/rookout/piper/pkg/workflow_handler"
12 | "golang.org/x/net/context"
13 | "log"
14 | "os/signal"
15 | "syscall"
16 | )
17 |
18 | func main() {
19 | cfg, err := conf.LoadConfig()
20 | if err != nil {
21 | log.Panicf("failed to load the configuration for Piper, error: %v", err)
22 | }
23 |
24 | if cfg.RookoutConfig.Token != "" {
25 | labels := utils.StringToMap(cfg.RookoutConfig.Labels)
26 | err = rookout.Start(rookout.RookOptions{Token: cfg.RookoutConfig.Token, Labels: labels})
27 | if err != nil {
28 | log.Printf("failed to start Rookout, error: %v\n", err)
29 | }
30 | }
31 |
32 | err = cfg.WorkflowsConfig.WorkflowsSpecLoad("/piper-config/..data")
33 | if err != nil {
34 | log.Panicf("Failed to load workflow spec configuration, error: %v", err)
35 | }
36 |
37 | gitProvider, err := git_provider.NewGitProviderClient(cfg)
38 | if err != nil {
39 | log.Panicf("failed to load the Git client for Piper, error: %v", err)
40 | }
41 | workflows, err := workflowHandler.NewWorkflowsClient(cfg)
42 | if err != nil {
43 | log.Panicf("failed to load the Argo Workflows client for Piper, error: %v", err)
44 | }
45 |
46 | globalClients := &clients.Clients{
47 | GitProvider: gitProvider,
48 | Workflows: workflows,
49 | }
50 |
51 | // Create context that listens for the interrupt signal from the OS.
52 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
53 | defer stop()
54 | event_handler.Start(ctx, stop, cfg, globalClients)
55 | server.Start(ctx, stop, cfg, globalClients)
56 | }
57 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | paths:
8 | - '**'
9 | - '!docs/**'
10 | pull_request:
11 | branches:
12 | - "main"
13 |
14 | concurrency:
15 | group: ${{ github.workflow }}-${{ github.ref }}
16 | cancel-in-progress: true
17 |
18 | permissions:
19 | contents: read
20 |
21 | jobs:
22 | tests:
23 | name: Unit Tests
24 | runs-on: ubuntu-latest
25 | timeout-minutes: 10
26 | steps:
27 | - uses: actions/checkout@v3
28 | - uses: actions/setup-go@v4
29 | with:
30 | go-version: "1.20"
31 | cache: true
32 | - run: make test
33 | lint:
34 | name: Go Lint
35 | runs-on: ubuntu-latest
36 | timeout-minutes: 10
37 | steps:
38 | - uses: actions/checkout@v3
39 | - uses: actions/setup-go@v4
40 | with:
41 | go-version: '1.20'
42 | cache: true
43 | - name: golangci-lint
44 | uses: golangci/golangci-lint-action@v3
45 | with:
46 | version: v1.53
47 | only-new-issues: true
48 | skip-pkg-cache: true
49 | args: --timeout=10m
50 | helm:
51 | name: Helm Lint
52 | runs-on: ubuntu-latest
53 | timeout-minutes: 10
54 | steps:
55 | - uses: actions/checkout@v3
56 | with:
57 | fetch-depth: 0
58 | - name: Check Git diff in /helm-chart
59 | run: |
60 | if [ "$(git diff --exit-code --name-only --diff-filter=d origin/main -- helm-chart/)" != "" ]; then
61 | echo "There are Git diffs in the /helm-chart folder."
62 | echo "CHART_UPDATED=true" >> $GITHUB_ENV
63 | else
64 | echo "There are no Git diffs in the /helm-chart folder."
65 | fi
66 | - name: Install Helm Docs
67 | uses: envoy/install-helm-docs@v1.0.0
68 | with:
69 | version: 1.11.0
70 | - name: Helm lint and template
71 | run: |
72 | make helm
73 | if: ${{ env.CHART_UPDATED }}
--------------------------------------------------------------------------------
/examples/workflow.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: Workflow
3 | metadata:
4 | generateName: test-
5 | labels:
6 | branch: test-branch
7 | commit: xxxxxxxxxxxxxx
8 | repo: somerepo
9 | user: gosharo
10 | spec:
11 | volumes:
12 | - name: shared-volume
13 | emptyDir: { }
14 | activeDeadlineSeconds: 7200 # (seconds) == 2 hours
15 | ttlStrategy:
16 | secondsAfterCompletion: 28800 # (seconds) == 8 hours
17 | podGC:
18 | strategy: OnPodSuccess
19 | archiveLogs: true
20 | arguments:
21 | parameters:
22 | - name: PLACHOLDER
23 | artifactRepositoryRef:
24 | configMap: artifact-repositories
25 | onExit: exit-handler
26 | entrypoint: entrypoint
27 | nodeSelector:
28 | node_pool: workflows
29 | serviceAccountName: argo-wf
30 | tolerations:
31 | - effect: NoSchedule
32 | key: node_pool
33 | operator: Equal
34 | value: workflows
35 | templates:
36 | - dag:
37 | name: exit-handler
38 | tasks:
39 | - name: github-status
40 | template: exit-handler
41 | arguments:
42 | parameters:
43 | - name: param1
44 | value: '{{ workflow.labels.repo }}'
45 | - name: local-step
46 | inputs:
47 | parameters:
48 | - name: message
49 | script:
50 | image: alpine
51 | command: [sh]
52 | source: |
53 | echo "wellcome to {{ workflow.parameters.global }}
54 | echo "{{ inputs.parameters.message }}"
55 | - name: exit-handler
56 | script:
57 | image: alpine
58 | command: [sh]
59 | source: |
60 | echo "exit"
61 | - dag:
62 | name: entrypoint
63 | tasks:
64 | - name: local-step1
65 | template: local-step
66 | arguments:
67 | parameters:
68 | - name: message
69 | value: step-1
70 | - name: local-step2
71 | template: local-step
72 | arguments:
73 | parameters:
74 | - name: message
75 | value: step-2
76 | dependencies:
77 | - local-step1
78 |
79 |
80 |
--------------------------------------------------------------------------------
/pkg/server/routes/webhook.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "github.com/rookout/piper/pkg/webhook_creator"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/rookout/piper/pkg/clients"
10 | "github.com/rookout/piper/pkg/conf"
11 | webhookHandler "github.com/rookout/piper/pkg/webhook_handler"
12 | )
13 |
14 | func AddWebhookRoutes(cfg *conf.GlobalConfig, clients *clients.Clients, rg *gin.RouterGroup, wc *webhook_creator.WebhookCreatorImpl) {
15 | webhook := rg.Group("/webhook")
16 |
17 | webhook.POST("", func(c *gin.Context) {
18 | ctx := c.Request.Context()
19 | webhookPayload, err := clients.GitProvider.HandlePayload(&ctx, c.Request, []byte(cfg.GitProviderConfig.WebhookSecret))
20 | if err != nil {
21 | log.Println(err)
22 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
23 | return
24 | }
25 |
26 | if webhookPayload.Event == "ping" {
27 | if cfg.GitProviderConfig.FullHealthCheck {
28 | err = wc.SetWebhookHealth(webhookPayload.HookID, true)
29 | if err != nil {
30 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
31 | return
32 | }
33 | }
34 | c.JSON(http.StatusOK, gin.H{"status": "ok"})
35 | return
36 | }
37 |
38 | wh, err := webhookHandler.NewWebhookHandler(cfg, clients, webhookPayload)
39 | if err != nil {
40 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
41 | log.Printf("failed to create webhook handler, error: %v", err)
42 | return
43 | }
44 |
45 | workflowsBatches, err := webhookHandler.HandleWebhook(&ctx, wh)
46 | if err != nil {
47 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
48 | log.Printf("failed to handle webhook, error: %v", err)
49 | return
50 | }
51 |
52 | for _, wf := range workflowsBatches {
53 | err = clients.Workflows.HandleWorkflowBatch(&ctx, wf)
54 | if err != nil {
55 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
56 | log.Printf("failed to handle workflow, error: %v", err)
57 | return
58 | }
59 | }
60 |
61 | c.JSON(http.StatusOK, gin.H{"status": "ok"})
62 | })
63 | }
64 |
--------------------------------------------------------------------------------
/docs/configuration/environment_variables.md:
--------------------------------------------------------------------------------
1 | ## Environment Variables
2 |
3 | The environment variables used by Piper to configure its functionality.
4 | The helm chart populate them using [values.yaml](https://github.com/Rookout/piper/tree/main/helm-chart/values.yaml) file
5 |
6 | ### Git
7 |
8 | * GIT_PROVIDER
9 | The git provider that Piper will use, possible variables: GitHub (will support bitbucket and gitlab)
10 |
11 | * GIT_TOKEN
12 | The git token that will be used.
13 |
14 | * GIT_ORG_NAME
15 | The organization name.
16 |
17 | * GIT_ORG_LEVEL_WEBHOOK
18 | Boolean variable, whether to config webhook in organization level. default `false`
19 |
20 | * GIT_WEBHOOK_REPO_LIST
21 | List of repositories to configure webhooks to.
22 |
23 | * GIT_WEBHOOK_URL
24 | URL of piper ingress, to configure webhooks.
25 |
26 | * GIT_WEBHOOK_AUTO_CLEANUP
27 | Will cleanup all webhook that were created with piper.
28 | Notice that there will be a race conditions between pod that being terminated and the new one.
29 |
30 | * GIT_ENFORCE_ORG_BELONGING
31 | Boolean variable, whether to enforce organizational belonging of git event creator. default `false`
32 |
33 | * GIT_FULL_HEALTH_CHECK
34 | Enables full health check of webhook. Full health check contains expecting and validating ping event from a webhook.
35 | Doesn't work for bitbucket, because the API call don't
36 |
37 |
38 | ### Argo Workflows Server
39 | * ARGO_WORKFLOWS_TOKEN
40 | The token of Argo Workflows server.
41 |
42 | * ARGO_WORKFLOWS_ADDRESS
43 | The address of Argo Workflows Server.
44 |
45 | * ARGO_WORKFLOWS_CREATE_CRD
46 | Whether to directly send Workflows instruction or create a CRD in the Cluster.
47 |
48 | * ARGO_WORKFLOWS_NAMESPACE
49 | The namespace of Workflows creation for Argo Workflows.
50 |
51 | * KUBE_CONFIG
52 | Used to configure Argo Workflows client with local kube configurations.
53 |
54 | ### Rookout
55 | * ROOKOUT_TOKEN
56 | The token used to configure Rookout agent. If not provided, will not start the agent.
57 | * ROOKOUT_LABELS
58 | The labels to label instances at Rookout, default to "service:piper"
59 | * ROOKOUT_REMOTE_ORIGIN
60 | The repo URL for source code fetching, default:"https://github.com/Rookout/piper.git".
--------------------------------------------------------------------------------
/.github/workflows/snyk.yaml:
--------------------------------------------------------------------------------
1 | name: Snyk Scan
2 |
3 | on:
4 | pull_request:
5 | branches: ["main"]
6 |
7 | permissions:
8 | contents: read
9 | jobs:
10 | snyk-container:
11 | permissions:
12 | contents: read # for actions/checkout to fetch code
13 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-go@v4
18 | with:
19 | go-version: "1.20"
20 | cache: true
21 | - uses: docker/setup-qemu-action@v2
22 | - uses: docker/setup-buildx-action@v2
23 | - name: Build Docker Image without push
24 | uses: docker/build-push-action@v4
25 | with:
26 | context: .
27 | push: false
28 | tags: rookout/piper:latest
29 | cache-from: type=gha
30 | cache-to: type=gha,mode=max
31 | load: true
32 | - name: Run Snyk to check Docker image for vulnerabilities
33 | uses: snyk/actions/docker@master
34 | env:
35 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
36 | with:
37 | image: rookout/piper:latest
38 | args: --file=Dockerfile --severity-threshold=high --sarif-file-output=snyk.sarif
39 | - name: Upload result to GitHub Code Scanning
40 | uses: github/codeql-action/upload-sarif@v2
41 | with:
42 | sarif_file: snyk.sarif
43 | snyk-golang:
44 | permissions:
45 | contents: read # for actions/checkout to fetch code
46 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
47 | runs-on: ubuntu-latest
48 | steps:
49 | - uses: actions/checkout@v3
50 | - uses: actions/setup-go@v4
51 | with:
52 | go-version: "1.20"
53 | cache: true
54 | - name: Run Snyk to check for vulnerabilities
55 | uses: snyk/actions/golang@master
56 | env:
57 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
58 | with:
59 | args: --sarif-file-output=snyk.sarif --severity-threshold=high
60 | - name: Upload result to GitHub Code Scanning
61 | uses: github/codeql-action/upload-sarif@v2
62 | with:
63 | sarif_file: snyk.sarif
--------------------------------------------------------------------------------
/helm-chart/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | {{- $fullName := include "piper.fullname" . -}}
3 | {{- $svcPort := .Values.service.port -}}
4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
7 | {{- end }}
8 | {{- end }}
9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
10 | apiVersion: networking.k8s.io/v1
11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
12 | apiVersion: networking.k8s.io/v1beta1
13 | {{- else -}}
14 | apiVersion: extensions/v1beta1
15 | {{- end }}
16 | kind: Ingress
17 | metadata:
18 | name: {{ $fullName }}
19 | labels:
20 | {{- include "piper.labels" . | nindent 4 }}
21 | {{- with .Values.ingress.annotations }}
22 | annotations:
23 | {{- toYaml . | nindent 4 }}
24 | {{- end }}
25 | spec:
26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
27 | ingressClassName: {{ .Values.ingress.className }}
28 | {{- end }}
29 | {{- if .Values.ingress.tls }}
30 | tls:
31 | {{- range .Values.ingress.tls }}
32 | - hosts:
33 | {{- range .hosts }}
34 | - {{ . | quote }}
35 | {{- end }}
36 | secretName: {{ .secretName }}
37 | {{- end }}
38 | {{- end }}
39 | rules:
40 | {{- range .Values.ingress.hosts }}
41 | - host: {{ .host | quote }}
42 | http:
43 | paths:
44 | {{- range .paths }}
45 | - path: {{ .path }}
46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
47 | pathType: {{ .pathType }}
48 | {{- end }}
49 | backend:
50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
51 | service:
52 | name: {{ $fullName }}
53 | port:
54 | number: {{ $svcPort }}
55 | {{- else }}
56 | serviceName: {{ $fullName }}
57 | servicePort: {{ $svcPort }}
58 | {{- end }}
59 | {{- end }}
60 | {{- end }}
61 | {{- end }}
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Piper - MOVED
2 |
3 | ⚠️ **Important Notice:** Piper has moved to a new and improved repository. Please visit the following link for the latest updates and contributions:
4 |
5 | [New repository](https://github.com/quickube/piper)
6 |
7 | ## Support and Contributions
8 |
9 | As we focus our efforts on the new repository, we won't be actively addressing issues or accepting pull requests here. Feel free to reach out to us on the new repository for any questions, issues, or contributions.
10 |
11 | Thank you for your support and understanding.
12 |
13 | Happy coding!
14 |
15 | [New Piper Repository](https://github.com/quickube/piper)
16 |
17 | ## Table of Contents
18 |
19 | - [Getting Started](#getting-started)
20 | - [Reporting Issues](#reporting-issues)
21 | - [How to Contribute](docs/CONTRIBUTING.md#how-to-contribute)
22 | - [License](#license)
23 |
24 | ## Getting Started
25 |
26 | Piper configures a webhook in git provider and listens to the webhooks sends. It will create a Workflow CRD out of branches that contains `.workflows` folder.
27 | This folder should contain declarations of the templates and main DAG that will be running.
28 | Finally, it will submit the Workflow as a K8s resource in the cluster.
29 | To access more detailed explanations, please navigate to the [Documentation site](https://piper.rookout.com).
30 |
31 | https://github.com/Rookout/piper/assets/106976988/09b3a5d8-3428-4bdc-9146-3034d81164bf
32 |
33 | ## Reporting Issues
34 |
35 | If you encounter any issues or bugs while using Piper, please help us improve by reporting them. Follow these steps to report an issue:
36 |
37 | 1. Go to the [Piper Issues](https://github.com/Rookout/Piper/issues) page on GitHub.
38 | 2. Click on the "New Issue" button.
39 | 3. Provide a descriptive title and detailed description of the issue, including any relevant error messages or steps to reproduce the problem.
40 | 4. Add appropriate labels to categorize the issue (e.g., bug, enhancement, question).
41 | 5. Submit the issue, and our team will review and address it as soon as possible.
42 |
43 | ## How to Contribute
44 |
45 | If you're interested in contributing to this project, please feel free to submit a pull request. We welcome all contributions and feedback.
46 | Please check out our [Contribution guidelines for this project](docs/CONTRIBUTING.md)
47 |
48 | ## License
49 |
50 | This project is licensed under the Apache License. Please see the [LICENSE](LICENSE) file for details.
51 |
--------------------------------------------------------------------------------
/pkg/git_provider/bitbucket_utils.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "fmt"
5 | bitbucket "github.com/ktrysmt/go-bitbucket"
6 | "github.com/rookout/piper/pkg/conf"
7 | "github.com/rookout/piper/pkg/utils"
8 | "log"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | func ValidateBitbucketPermissions(client *bitbucket.Client, cfg *conf.GlobalConfig) error {
14 |
15 | repoAdminScopes := []string{"webhook", "repository:admin", "pullrequest:write"}
16 | repoGranularScopes := []string{"webhook", "repository", "pullrequest"}
17 |
18 | scopes, err := GetBitbucketTokenScopes(client, cfg)
19 |
20 | if err != nil {
21 | return fmt.Errorf("failed to get scopes: %v", err)
22 | }
23 | if len(scopes) == 0 {
24 | return fmt.Errorf("permissions error: no scopes found for the github client")
25 | }
26 |
27 | if utils.ListContains(repoAdminScopes, scopes) {
28 | return nil
29 | }
30 | if utils.ListContains(repoGranularScopes, scopes) {
31 | return nil
32 | }
33 |
34 | return fmt.Errorf("permissions error: %v is not a valid scopes", scopes)
35 | }
36 |
37 | func GetBitbucketTokenScopes(client *bitbucket.Client, cfg *conf.GlobalConfig) ([]string, error) {
38 |
39 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/repositories/%s", client.GetApiBaseURL(), cfg.GitProviderConfig.OrgName), nil)
40 | if err != nil {
41 | log.Println("Error creating request:", err)
42 | return nil, err
43 | }
44 | req.Header.Set("Accept", "application/json")
45 | req.Header.Set("Authorization", "Bearer "+cfg.GitProviderConfig.Token)
46 |
47 | resp, err := client.HttpClient.Do(req)
48 | if err != nil {
49 | log.Println("Error making request:", err)
50 | return nil, err
51 | }
52 | defer resp.Body.Close()
53 |
54 | if resp.StatusCode != 200 {
55 | return nil, fmt.Errorf("token validation failed: %v", resp.Status)
56 | }
57 |
58 | // Check the "X-OAuth-Scopes" header to get the token scopes
59 | acceptedScopes := resp.Header.Get("X-Accepted-OAuth-Scopes")
60 | scopes := resp.Header.Get("X-OAuth-Scopes")
61 | log.Println("Bitbucket Token Scopes are:", scopes, acceptedScopes)
62 |
63 | scopes = strings.ReplaceAll(scopes, " ", "")
64 | return append(strings.Split(scopes, ","), acceptedScopes), nil
65 |
66 | }
67 |
68 | func addHookToHashTable(hookUuid string, hookHashTable map[string]int64) {
69 | hookHashTable[hookUuid] = utils.StringToInt64(hookUuid)
70 | }
71 |
72 | func getHookByUUID(hookUuid string, hookHashTable map[string]int64) (int64, error) {
73 | res, ok := hookHashTable[hookUuid]
74 | if !ok {
75 | return 0, fmt.Errorf("hookUuid %s not found", hookUuid)
76 | }
77 | return res, nil
78 | }
79 |
--------------------------------------------------------------------------------
/pkg/event_handler/github_event_notifier.go:
--------------------------------------------------------------------------------
1 | package event_handler
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
7 | "github.com/rookout/piper/pkg/clients"
8 | "github.com/rookout/piper/pkg/conf"
9 | "github.com/rookout/piper/pkg/utils"
10 | )
11 |
12 | var workflowTranslationToGithubMap = map[string]string{
13 | "": "pending",
14 | "Pending": "pending",
15 | "Running": "pending",
16 | "Succeeded": "success",
17 | "Failed": "failure",
18 | "Error": "error",
19 | }
20 |
21 | var workflowTranslationToBitbucketMap = map[string]string{
22 | "": "INPROGRESS",
23 | "Pending": "INPROGRESS",
24 | "Running": "INPROGRESS",
25 | "Succeeded": "SUCCESSFUL",
26 | "Failed": "FAILED",
27 | "Error": "STOPPED",
28 | }
29 |
30 | type githubNotifier struct {
31 | cfg *conf.GlobalConfig
32 | clients *clients.Clients
33 | }
34 |
35 | func NewGithubEventNotifier(cfg *conf.GlobalConfig, clients *clients.Clients) EventNotifier {
36 | return &githubNotifier{
37 | cfg: cfg,
38 | clients: clients,
39 | }
40 | }
41 |
42 | func (gn *githubNotifier) Notify(ctx *context.Context, workflow *v1alpha1.Workflow) error {
43 | fmt.Printf("Notifing workflow, %s\n", workflow.GetName())
44 |
45 | repo, ok := workflow.GetLabels()["repo"]
46 | if !ok {
47 | return fmt.Errorf("failed get repo label for workflow: %s", workflow.GetName())
48 | }
49 | commit, ok := workflow.GetLabels()["commit"]
50 | if !ok {
51 | return fmt.Errorf("failed get commit label for workflow: %s", workflow.GetName())
52 | }
53 |
54 | workflowLink := fmt.Sprintf("%s/workflows/%s/%s", gn.cfg.WorkflowServerConfig.ArgoAddress, gn.cfg.Namespace, workflow.GetName())
55 |
56 | status, err := gn.translateWorkflowStatus(string(workflow.Status.Phase), workflow.GetName())
57 | if err != nil {
58 | return err
59 | }
60 |
61 | message := utils.TrimString(workflow.Status.Message, 140) // Max length of message is 140 characters
62 | err = gn.clients.GitProvider.SetStatus(ctx, &repo, &commit, &workflowLink, &status, &message)
63 | if err != nil {
64 | return fmt.Errorf("failed to set status for workflow %s: %s", workflow.GetName(), err)
65 | }
66 |
67 | return nil
68 | }
69 |
70 | func (gn *githubNotifier) translateWorkflowStatus(status string, workflowName string) (string, error) {
71 | switch gn.cfg.GitProviderConfig.Provider {
72 | case "github":
73 | result, ok := workflowTranslationToGithubMap[status]
74 | if !ok {
75 | return "", fmt.Errorf("failed to translate workflow status to github stasuts for %s status: %s", workflowName, status)
76 | }
77 | return result, nil
78 | case "bitbucket":
79 | result, ok := workflowTranslationToBitbucketMap[status]
80 | if !ok {
81 | return "", fmt.Errorf("failed to translate workflow status to bitbucket stasuts for %s status: %s", workflowName, status)
82 | }
83 | return result, nil
84 | }
85 | return "", fmt.Errorf("failed to translate workflow status")
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/git_provider/github_utils_test.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "testing"
9 |
10 | "github.com/google/go-github/v52/github"
11 | "github.com/rookout/piper/pkg/conf"
12 | "github.com/rookout/piper/pkg/utils"
13 | assertion "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestIsOrgWebhookEnabled(t *testing.T) {
17 | //
18 | // Prepare
19 | //
20 | client, mux, _, teardown := setup()
21 | defer teardown()
22 |
23 | config := make(map[string]interface{})
24 | config["url"] = "https://bla.com"
25 | Hooks := github.Hook{
26 | Active: utils.BPtr(true),
27 | Name: utils.SPtr("web"),
28 | Config: config,
29 | }
30 | jsonBytes, _ := json.Marshal(&[]github.Hook{Hooks})
31 |
32 | mux.HandleFunc("/orgs/test/hooks", func(w http.ResponseWriter, r *http.Request) {
33 | testMethod(t, r, "GET")
34 | testFormValues(t, r, values{})
35 | _, _ = fmt.Fprint(w, string(jsonBytes))
36 | })
37 |
38 | c := GithubClientImpl{
39 | client: client,
40 | cfg: &conf.GlobalConfig{
41 | GitProviderConfig: conf.GitProviderConfig{
42 | OrgLevelWebhook: true,
43 | OrgName: "test",
44 | WebhookURL: "https://bla.com",
45 | },
46 | },
47 | }
48 | ctx := context.Background()
49 |
50 | //
51 | // Execute
52 | //
53 | hooks, isEnabled := isOrgWebhookEnabled(ctx, &c)
54 |
55 | //
56 | // Assert
57 | //
58 | assert := assertion.New(t)
59 | assert.True(isEnabled)
60 | assert.NotNil(t, hooks)
61 | }
62 |
63 | func TestIsRepoWebhookEnabled(t *testing.T) {
64 | //
65 | // Prepare
66 | //
67 | client, mux, _, teardown := setup()
68 | defer teardown()
69 |
70 | config := make(map[string]interface{})
71 | config["url"] = "https://bla.com"
72 | Hooks := github.Hook{
73 | Active: utils.BPtr(true),
74 | Name: utils.SPtr("web"),
75 | Config: config,
76 | }
77 | jsonBytes, _ := json.Marshal(&[]github.Hook{Hooks})
78 |
79 | mux.HandleFunc("/repos/test/test-repo2/hooks", func(w http.ResponseWriter, r *http.Request) {
80 | testMethod(t, r, "GET")
81 | testFormValues(t, r, values{})
82 | _, _ = fmt.Fprint(w, string(jsonBytes))
83 | })
84 |
85 | c := GithubClientImpl{
86 | client: client,
87 | cfg: &conf.GlobalConfig{
88 | GitProviderConfig: conf.GitProviderConfig{
89 | OrgLevelWebhook: false,
90 | OrgName: "test",
91 | WebhookURL: "https://bla.com",
92 | RepoList: "test-repo1,test-repo2",
93 | },
94 | },
95 | }
96 | ctx := context.Background()
97 |
98 | //
99 | // Execute
100 | //
101 | hook, isEnabled := isRepoWebhookEnabled(ctx, &c, "test-repo2")
102 |
103 | //
104 | // Assert
105 | //
106 | assert := assertion.New(t)
107 | assert.True(isEnabled)
108 | assert.NotNil(t, hook)
109 |
110 | //
111 | // Execute
112 | //
113 | hook, isEnabled = isRepoWebhookEnabled(ctx, &c, "test-repo3")
114 |
115 | //
116 | // Assert
117 | //
118 | assert = assertion.New(t)
119 | assert.False(isEnabled)
120 | assert.NotNil(t, hook)
121 | }
122 |
--------------------------------------------------------------------------------
/helm-chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "piper.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Return secret name to be used based on provided values.
10 | */}}
11 | {{- define "piper.argoWorkflows.tokenSecretName" -}}
12 | {{- $fullName := printf "%s-argo-token" .Release.Name -}}
13 | {{- default $fullName .Values.piper.argoWorkflows.server.existingSecret | quote -}}
14 | {{- end -}}
15 |
16 | {{/*
17 | Return secret name to be used based on provided values.
18 | */}}
19 | {{- define "piper.gitProvider.tokenSecretName" -}}
20 | {{- $fullName := printf "%s-git-token" .Release.Name -}}
21 | {{- default $fullName .Values.piper.gitProvider.existingSecret | quote -}}
22 | {{- end -}}
23 |
24 | {{/*
25 | Return secret name to be used based on provided values.
26 | */}}
27 | {{- define "piper.gitProvider.webhook.secretName" -}}
28 | {{- $fullName := printf "%s-webhook-secret" .Release.Name -}}
29 | {{- default $fullName .Values.piper.gitProvider.webhook.existingSecret | quote -}}
30 | {{- end -}}
31 |
32 | {{/*
33 | Return secret name to be used based on provided values.
34 | */}}
35 | {{- define "rookout.secretName" -}}
36 | {{- $fullName := printf "%s-rookout-token" .Release.Name -}}
37 | {{- default $fullName .Values.rookout.existingSecret | quote -}}
38 | {{- end -}}
39 |
40 |
41 | {{/*
42 | Create a default fully qualified app name.
43 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
44 | If release name contains chart name it will be used as a full name.
45 | */}}
46 | {{- define "piper.fullname" -}}
47 | {{- if .Values.fullnameOverride }}
48 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
49 | {{- else }}
50 | {{- $name := default .Chart.Name .Values.nameOverride }}
51 | {{- if contains $name .Release.Name }}
52 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
53 | {{- else }}
54 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
55 | {{- end }}
56 | {{- end }}
57 | {{- end }}
58 |
59 | {{/*
60 | Create chart name and version as used by the chart label.
61 | */}}
62 | {{- define "piper.chart" -}}
63 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
64 | {{- end }}
65 |
66 | {{/*
67 | Common labels
68 | */}}
69 | {{- define "piper.labels" -}}
70 | helm.sh/chart: {{ include "piper.chart" . }}
71 | {{ include "piper.selectorLabels" . }}
72 |
73 | {{- if .Chart.AppVersion }}
74 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
75 | {{- end }}
76 | app.kubernetes.io/managed-by: {{ .Release.Service }}
77 | {{- end }}
78 |
79 | {{/*
80 | Selector labels
81 | */}}
82 | {{- define "piper.selectorLabels" -}}
83 | app.kubernetes.io/name: {{ include "piper.name" . }}
84 | app.kubernetes.io/instance: {{ .Release.Name }}
85 | {{- end }}
86 |
87 | {{/*
88 | Create the name of the service account to use
89 | */}}
90 | {{- define "piper.serviceAccountName" -}}
91 | {{- if .Values.serviceAccount.create }}
92 | {{- default (include "piper.fullname" .) .Values.serviceAccount.name }}
93 | {{- else }}
94 | {{- default "default" .Values.serviceAccount.name }}
95 | {{- end }}
96 | {{- end }}
97 |
--------------------------------------------------------------------------------
/pkg/git_provider/test_utils.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "fmt"
5 | "github.com/google/go-cmp/cmp"
6 | "github.com/google/go-github/v52/github"
7 | "github.com/ktrysmt/go-bitbucket"
8 | "net/http"
9 | "net/http/httptest"
10 | "net/url"
11 | "os"
12 | "testing"
13 | )
14 |
15 | const (
16 | // baseURLPath is a non-empty Client.BaseURL path to use during tests,
17 | // to ensure relative URLs are used for all endpoints. See issue #752.
18 | baseURLPath = "/api-v3"
19 | bitbucketBaseURLPath = "/2.0"
20 | )
21 |
22 | func setup() (client *github.Client, mux *http.ServeMux, serverURL string, teardown func()) {
23 | mux = http.NewServeMux()
24 |
25 | apiHandler := http.NewServeMux()
26 | apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, mux))
27 | apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
28 | fmt.Fprintln(os.Stderr, "FAIL: Client.BaseURL path prefix is not preserved in the request URL:")
29 | fmt.Fprintln(os.Stderr)
30 | fmt.Fprintln(os.Stderr, "\t"+req.URL.String())
31 | fmt.Fprintln(os.Stderr)
32 | fmt.Fprintln(os.Stderr, "\tDid you accidentally use an absolute endpoint URL rather than relative?")
33 | fmt.Fprintln(os.Stderr, "\tSee https://github.com/google/go-github/issues/752 for information.")
34 | http.Error(w, "Client.BaseURL path prefix is not preserved in the request URL.", http.StatusInternalServerError)
35 | })
36 |
37 | // server is a test HTTP server used to provide mock API responses.
38 | server := httptest.NewServer(apiHandler)
39 |
40 | // client is the GitHub client being tested and is
41 | // configured to use test server.
42 | client = github.NewClient(nil)
43 | url, _ := url.Parse(server.URL + baseURLPath + "/")
44 | client.BaseURL = url
45 | client.UploadURL = url
46 |
47 | return client, mux, server.URL, server.Close
48 | }
49 |
50 | func testMethod(t *testing.T, r *http.Request, want string) {
51 | t.Helper()
52 | if got := r.Method; got != want {
53 | t.Errorf("Request method: %v, want %v", got, want)
54 | }
55 | }
56 |
57 | type values map[string]string
58 |
59 | func testFormValues(t *testing.T, r *http.Request, values values) {
60 | t.Helper()
61 | want := url.Values{}
62 | for k, v := range values {
63 | want.Set(k, v)
64 | }
65 |
66 | err := r.ParseForm()
67 | if err != nil {
68 | t.Errorf("Go error parsing form: %v", err)
69 | }
70 | if got := r.Form; !cmp.Equal(got, want) {
71 | t.Errorf("Request parameters: %v, want %v", got, want)
72 | }
73 | }
74 |
75 | func setupBitbucket() (client *bitbucket.Client, mux *http.ServeMux, serverURL string, teardown func()) {
76 | mux = http.NewServeMux()
77 |
78 | apiHandler := http.NewServeMux()
79 | apiHandler.Handle(bitbucketBaseURLPath+"/", http.StripPrefix(bitbucketBaseURLPath, mux))
80 | apiHandler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
81 | http.Error(w, "Client.BaseURL path prefix is not preserved in the request URL.", http.StatusInternalServerError)
82 | })
83 |
84 | server := httptest.NewServer(apiHandler)
85 | url, _ := url.Parse(server.URL + bitbucketBaseURLPath)
86 | client = bitbucket.NewBasicAuth("username", "password")
87 | client.SetApiBaseURL(*url)
88 |
89 | return client, mux, server.URL, server.Close
90 | }
91 |
--------------------------------------------------------------------------------
/scripts/init-kind.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -o errexit
3 |
4 | # 1. Create registry container unless it already exists
5 | reg_name='kind-registry'
6 | reg_port='5001'
7 | if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
8 | docker run \
9 | -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \
10 | registry:2
11 | fi
12 |
13 | # 2. Create kind cluster with containerd registry config dir enabled
14 |
15 | if [ $(kind get clusters | grep "piper") = ""]; then
16 | cat <" ]; do
80 | sleep 0.1;
81 | done;
82 |
83 | # waiting for core dns to up - to indicate that scheduling can happen
84 | echo "waiting core dns to up"
85 | until [ "`kubectl rollout status deployment --namespace kube-system | grep coredns`"=="deployment "coredns" successfully rolled out" ]; do
86 | sleep 0.1;
87 | done;
88 |
89 | kubectl wait --namespace kube-system \
90 | --for=condition=ready pod \
91 | --selector=k8s-app=kube-dns \
92 | --timeout=30s
93 |
--------------------------------------------------------------------------------
/pkg/webhook_creator/mocks.go:
--------------------------------------------------------------------------------
1 | package webhook_creator
2 |
3 | import (
4 | "errors"
5 | "github.com/rookout/piper/pkg/git_provider"
6 | "golang.org/x/net/context"
7 | "net/http"
8 | )
9 |
10 | type MockGitProviderClient struct {
11 | ListFilesFunc func(ctx context.Context, repo string, branch string, path string) ([]string, error)
12 | GetFileFunc func(ctx context.Context, repo string, branch string, path string) (*git_provider.CommitFile, error)
13 | GetFilesFunc func(ctx context.Context, repo string, branch string, paths []string) ([]*git_provider.CommitFile, error)
14 | SetWebhookFunc func(ctx context.Context, repo *string) (*git_provider.HookWithStatus, error)
15 | UnsetWebhookFunc func(ctx context.Context, hook *git_provider.HookWithStatus) error
16 | HandlePayloadFunc func(request *http.Request, secret []byte) (*git_provider.WebhookPayload, error)
17 | SetStatusFunc func(ctx context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error
18 | PingHookFunc func(ctx context.Context, hook *git_provider.HookWithStatus) error
19 | }
20 |
21 | func (m *MockGitProviderClient) ListFiles(ctx *context.Context, repo string, branch string, path string) ([]string, error) {
22 | if m.ListFilesFunc != nil {
23 | return m.ListFilesFunc(*ctx, repo, branch, path)
24 | }
25 | return nil, errors.New("unimplemented")
26 | }
27 |
28 | func (m *MockGitProviderClient) GetFile(ctx *context.Context, repo string, branch string, path string) (*git_provider.CommitFile, error) {
29 | if m.GetFileFunc != nil {
30 | return m.GetFileFunc(*ctx, repo, branch, path)
31 | }
32 | return nil, errors.New("unimplemented")
33 | }
34 |
35 | func (m *MockGitProviderClient) GetFiles(ctx *context.Context, repo string, branch string, paths []string) ([]*git_provider.CommitFile, error) {
36 | if m.GetFilesFunc != nil {
37 | return m.GetFilesFunc(*ctx, repo, branch, paths)
38 | }
39 | return nil, errors.New("unimplemented")
40 | }
41 |
42 | func (m *MockGitProviderClient) SetWebhook(ctx *context.Context, repo *string) (*git_provider.HookWithStatus, error) {
43 | if m.SetWebhookFunc != nil {
44 | return m.SetWebhookFunc(*ctx, repo)
45 | }
46 | return nil, errors.New("unimplemented")
47 | }
48 |
49 | func (m *MockGitProviderClient) UnsetWebhook(ctx *context.Context, hook *git_provider.HookWithStatus) error {
50 | if m.UnsetWebhookFunc != nil {
51 | return m.UnsetWebhookFunc(*ctx, hook)
52 | }
53 | return errors.New("unimplemented")
54 | }
55 |
56 | func (m *MockGitProviderClient) HandlePayload(ctx *context.Context, request *http.Request, secret []byte) (*git_provider.WebhookPayload, error) {
57 | if m.HandlePayloadFunc != nil {
58 | return m.HandlePayloadFunc(request, secret)
59 | }
60 | return nil, errors.New("unimplemented")
61 | }
62 |
63 | func (m *MockGitProviderClient) SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error {
64 | if m.SetStatusFunc != nil {
65 | return m.SetStatusFunc(*ctx, repo, commit, linkURL, status, message)
66 | }
67 | return errors.New("unimplemented")
68 | }
69 |
70 | func (m *MockGitProviderClient) PingHook(ctx *context.Context, hook *git_provider.HookWithStatus) error {
71 | if m.PingHookFunc != nil {
72 | return m.PingHookFunc(*ctx, hook)
73 | }
74 | return errors.New("unimplemented")
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/utils/os_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "os"
7 | "path/filepath"
8 | "testing"
9 | )
10 |
11 | func createFileWithContent(path string, content string) error {
12 | file, err := os.Create(path)
13 | if err != nil {
14 | return err
15 | }
16 | defer file.Close()
17 |
18 | _, err = io.WriteString(file, content)
19 | return err
20 | }
21 |
22 | func TestGetFilesData(t *testing.T) {
23 | // Create a temporary directory for testing
24 | tempDir := os.TempDir()
25 | testDir := filepath.Join(tempDir, "test")
26 | err := os.Mkdir(testDir, 0755)
27 | if err != nil {
28 | t.Fatalf("Error creating temporary directory: %v", err)
29 | }
30 | defer os.RemoveAll(testDir) // Clean up the temporary directory
31 |
32 | // Create some dummy files in the test directory
33 | file1Path := filepath.Join(testDir, "file1.txt")
34 | err = createFileWithContent(file1Path, "File 1 data")
35 | if err != nil {
36 | t.Fatalf("Error creating file1: %v", err)
37 | }
38 |
39 | file2Path := filepath.Join(testDir, "file2.txt")
40 | err = createFileWithContent(file2Path, "File 2 data")
41 | if err != nil {
42 | t.Fatalf("Error creating file2: %v", err)
43 | }
44 |
45 | // Call the function being tested
46 | fileData, err := GetFilesData(testDir)
47 | if err != nil {
48 | t.Fatalf("Error calling GetFilesData: %v", err)
49 | }
50 |
51 | // Verify the results
52 | expectedData := map[string][]byte{
53 | "file1.txt": []byte("File 1 data"),
54 | "file2.txt": []byte("File 2 data"),
55 | }
56 |
57 | for fileName, expected := range expectedData {
58 | actual, ok := fileData[fileName]
59 | if !ok {
60 | t.Errorf("Missing file data for %s", fileName)
61 | }
62 |
63 | if !bytes.Equal(actual, expected) {
64 | t.Errorf("File data mismatch for %s: expected '%s', got '%s'", fileName, expected, actual)
65 | }
66 | }
67 | }
68 |
69 | func stringSlicesEqual(a, b []string) bool {
70 | if len(a) != len(b) {
71 | return false
72 | }
73 | for i := range a {
74 | if a[i] != b[i] {
75 | return false
76 | }
77 | }
78 | return true
79 | }
80 |
81 | func TestGetFilesInLinkDirectory(t *testing.T) {
82 | tempDir := t.TempDir()
83 |
84 | fileNames := []string{"file1.txt", "file2.txt", "file3.txt"}
85 | for _, name := range fileNames {
86 | filePath := filepath.Join(tempDir, name)
87 | _, err := os.Create(filePath)
88 | if err != nil {
89 | t.Fatalf("Failed to create test file: %v", err)
90 | }
91 | }
92 |
93 | linkPath := filepath.Join(tempDir, "symlink")
94 | err := os.Symlink(tempDir, linkPath)
95 | if err != nil {
96 | t.Fatalf("Failed to create symbolic link: %v", err)
97 | }
98 |
99 | realPath, result, err := GetFilesInLinkDirectory(linkPath)
100 | if err != nil {
101 | t.Fatalf("Unexpected error: %v", err)
102 | }
103 |
104 | expectedRealPath, err := filepath.EvalSymlinks(linkPath)
105 | if err != nil {
106 | t.Fatalf("Failed to evaluate symlink: %v", err)
107 | }
108 | if *realPath != expectedRealPath {
109 | t.Errorf("Unexpected real path. Expected: %s, got: %s", expectedRealPath, *realPath)
110 | }
111 |
112 | t.Logf("Result: %v", result) // Add this line for debugging
113 |
114 | if !stringSlicesEqual(result, fileNames) {
115 | t.Errorf("Unexpected file names. Expected: %v, got: %v", fileNames, result)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/pkg/workflow_handler/workflows_utils.go:
--------------------------------------------------------------------------------
1 | package workflow_handler
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
7 | "github.com/rookout/piper/pkg/conf"
8 | "github.com/rookout/piper/pkg/git_provider"
9 | "github.com/rookout/piper/pkg/utils"
10 | "gopkg.in/yaml.v3"
11 | "log"
12 | "regexp"
13 | "strings"
14 | )
15 |
16 | func CreateDAGTemplate(fileList []*git_provider.CommitFile, name string) (*v1alpha1.Template, error) {
17 | if len(fileList) == 0 {
18 | log.Printf("empty file list for %s", name)
19 | return nil, nil
20 | }
21 | DAGs := make([]v1alpha1.DAGTask, 0)
22 | for _, file := range fileList {
23 | if file.Content == nil || file.Path == nil {
24 | return nil, fmt.Errorf("missing content or path for %s", name)
25 | }
26 | DAGTask := make([]v1alpha1.DAGTask, 0)
27 | jsonBytes, err := utils.ConvertYAMLListToJSONList(*file.Content)
28 | if err != nil {
29 | return nil, err
30 | }
31 | err = json.Unmarshal(jsonBytes, &DAGTask)
32 | if err != nil {
33 | return nil, err
34 | }
35 | err = ValidateDAGTasks(DAGTask)
36 | if err != nil {
37 | return nil, err
38 | }
39 | DAGs = append(DAGs, DAGTask...)
40 | }
41 |
42 | if len(DAGs) == 0 {
43 | return nil, fmt.Errorf("no tasks for %s", name)
44 | }
45 |
46 | template := &v1alpha1.Template{
47 | Name: name,
48 | DAG: &v1alpha1.DAGTemplate{
49 | Tasks: DAGs,
50 | },
51 | }
52 |
53 | return template, nil
54 | }
55 |
56 | func AddFilesToTemplates(templates []v1alpha1.Template, files []*git_provider.CommitFile) ([]v1alpha1.Template, error) {
57 | for _, f := range files {
58 | t := make([]v1alpha1.Template, 0)
59 | jsonBytes, err := utils.ConvertYAMLListToJSONList(*f.Content)
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | err = json.Unmarshal(jsonBytes, &t)
65 | if err != nil {
66 | return nil, err
67 | }
68 | templates = append(templates, t...)
69 | }
70 | return templates, nil
71 | }
72 |
73 | func GetParameters(paramsFile *git_provider.CommitFile) ([]v1alpha1.Parameter, error) {
74 | var params []v1alpha1.Parameter
75 | err := yaml.Unmarshal([]byte(*paramsFile.Content), ¶ms)
76 | if err != nil {
77 | return nil, err
78 | }
79 | return params, nil
80 | }
81 |
82 | func IsConfigExists(cfg *conf.WorkflowsConfig, config string) bool {
83 | _, ok := cfg.Configs[config]
84 | return ok
85 | }
86 |
87 | func IsConfigsOnExitExists(cfg *conf.WorkflowsConfig, config string) bool {
88 | return len(cfg.Configs[config].OnExit) != 0
89 | }
90 |
91 | func ValidateDAGTasks(tasks []v1alpha1.DAGTask) error {
92 | for _, task := range tasks {
93 | if task.Name == "" {
94 | return fmt.Errorf("task name cannot be empty: %+v\n", task)
95 | }
96 |
97 | if task.Template == "" && task.TemplateRef == nil {
98 | return fmt.Errorf("task template or templateRef cannot be empty: %+v\n", task)
99 | }
100 |
101 | }
102 | return nil
103 | }
104 |
105 | func ConvertToValidString(input string) string {
106 | // Convert to lowercase
107 | lowercase := strings.ToLower(input)
108 |
109 | // Replace underscores with hyphens
110 | converted := strings.ReplaceAll(lowercase, "_", "-")
111 |
112 | // Remove symbols except . and -
113 | pattern := `[^a-z0-9.\-]`
114 | re := regexp.MustCompile(pattern)
115 | validString := re.ReplaceAllString(converted, "")
116 |
117 | return validString
118 | }
119 |
--------------------------------------------------------------------------------
/docs/usage/workflows_folder.md:
--------------------------------------------------------------------------------
1 | ## .workflows Folder
2 |
3 | Piper will look in each of the target branches for a `.workflows` folder. [example](https://github.com/Rookout/piper/tree/main/examples/.workflows).
4 | We will explain each of the files that should be included in the `.workflows` folder.
5 |
6 | ### triggers.yaml (convention name)
7 |
8 | This file holds a list of triggers that will be executed `onStart` by `events` from specific `branches`.
9 | Piper will execute each of matching triggers, so configure it wisely.
10 | ```yaml
11 | - events:
12 | - push
13 | - pull_request.synchronize
14 | branches: ["main"]
15 | onStart: ["main.yaml"]
16 | onExit: ["exit.yaml"]
17 | templates: ["templates.yaml"]
18 | config: "default"
19 | ```
20 | Can be found [here](https://github.com/Rookout/piper/tree/main/examples/.workflows/triggers.yaml).
21 |
22 | In this example `main.yaml` will be executed as DAG when `push` or `pull_request.synchronize` events will be applied in `main` branch.
23 | `onExit` will be executed `exit.yaml` when finished the workflow as exit handler.
24 |
25 |
26 | `onExit` can overwrite the default `onExit` configuration from by reference existing DAG tasks as in the [example](https://github.com/Rookout/piper/tree/main/examples/.workflows/exit.yaml).
27 |
28 | `config` field used for workflow configuration selection. the default value is `default` configuration.
29 |
30 | #### events
31 | Events field used to terminate when the trigger will be executed. name of the event depends on the git provider.
32 |
33 | For instance, GitHub pull_request event have few action, one of them is synchronize.
34 |
35 | #### branches
36 | For which branch that trigger will be executed.
37 |
38 | #### onStart
39 | This [file](https://github.com/Rookout/piper/tree/main/examples/.workflows/main.yaml) can be named as you wish and will be referenced in `triggers.yaml` file. It will define an entrypoint DAG that the Workflow will execute.
40 |
41 | As a best practice, this file should contain the dependencies logic and parametrization of each of referenced templates. It should not implement new templates, for this, use template.yaml file.
42 |
43 | #### onExit
44 | This field used to pass verbose exitHandler to the triggered workflow.
45 | It will override the default onExit from the provided `config` or the default `config`.
46 |
47 | In the provided `exit.yaml` describes a DAG that will overwrite the default `onExit` configuration.
48 | [Example](https://github.com/Rookout/piper/tree/main/examples/.workflows/exit.yaml)
49 |
50 | #### templates
51 | This field will have additional templates that will be injected to the workflows.
52 | The purpose of this field is to create repository scope templates that can be referenced from the DAGs templates at `onStart` or `onExit`.
53 | [Example](https://github.com/Rookout/piper/tree/main/examples/.workflows/templates.yaml)
54 |
55 | As a best practice, use this field for template implementation and reference them from executed.
56 | [Example](https://github.com/Rookout/piper/tree/main/examples/.workflows/main.yaml).
57 |
58 | ### config
59 | configured by `piper-workflows-config` [configMap](workflows_config.md).
60 | Can be passed explicitly, or will use `deafault` configuration.
61 |
62 | ### parameters.yaml (convention name)
63 | Will hold a list of global parameters of the Workflow.
64 | can be referenced from any template with `{{ workflow.parameters.___ }}.`
65 |
66 | [Example](https://github.com/Rookout/piper/tree/main/examples/.workflows/parameters.yaml)
--------------------------------------------------------------------------------
/pkg/git_provider/github_utils.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 |
9 | "github.com/rookout/piper/pkg/utils"
10 |
11 | "github.com/google/go-github/v52/github"
12 | "github.com/rookout/piper/pkg/conf"
13 | )
14 |
15 | func isOrgWebhookEnabled(ctx context.Context, c *GithubClientImpl) (*github.Hook, bool) {
16 | emptyHook := github.Hook{}
17 | hooks, resp, err := c.client.Organizations.ListHooks(ctx, c.cfg.GitProviderConfig.OrgName, &github.ListOptions{})
18 | if err != nil {
19 | return &emptyHook, false
20 | }
21 | if resp.StatusCode != 200 {
22 | return &emptyHook, false
23 | }
24 | if len(hooks) == 0 {
25 | return &emptyHook, false
26 | }
27 | for _, hook := range hooks {
28 | if hook.GetActive() && hook.GetName() == "web" && hook.Config["url"] == c.cfg.GitProviderConfig.WebhookURL {
29 | return hook, true
30 | }
31 | }
32 | return &emptyHook, false
33 | }
34 |
35 | func isRepoWebhookEnabled(ctx context.Context, c *GithubClientImpl, repo string) (*github.Hook, bool) {
36 | emptyHook := github.Hook{}
37 | hooks, resp, err := c.client.Repositories.ListHooks(ctx, c.cfg.GitProviderConfig.OrgName, repo, &github.ListOptions{})
38 | if err != nil {
39 | return &emptyHook, false
40 | }
41 | if resp.StatusCode != 200 {
42 | return &emptyHook, false
43 | }
44 | if len(hooks) == 0 {
45 | return &emptyHook, false
46 | }
47 |
48 | for _, hook := range hooks {
49 | if hook.GetActive() && hook.GetName() == "web" && hook.Config["url"] == c.cfg.GitProviderConfig.WebhookURL {
50 | return hook, true
51 | }
52 | }
53 |
54 | return &emptyHook, false
55 | }
56 |
57 | func GetScopes(ctx context.Context, client *github.Client) ([]string, error) {
58 | // Make a request to the "Get the authenticated user" endpoint
59 | req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
60 | if err != nil {
61 | fmt.Println("Error creating request:", err)
62 | return nil, err
63 | }
64 | resp, err := client.Do(ctx, req, nil)
65 | if err != nil {
66 | fmt.Println("Error making request:", err)
67 | return nil, err
68 | }
69 | defer resp.Body.Close()
70 |
71 | // Check the "X-OAuth-Scopes" header to get the token scopes
72 | scopes := resp.Header.Get("X-OAuth-Scopes")
73 | fmt.Println("Github Token Scopes are:", scopes)
74 |
75 | scopes = strings.ReplaceAll(scopes, " ", "")
76 | return strings.Split(scopes, ","), nil
77 |
78 | }
79 |
80 | func ValidatePermissions(ctx context.Context, client *github.Client, cfg *conf.GlobalConfig) error {
81 |
82 | orgScopes := []string{"admin:org_hook"}
83 | repoAdminScopes := []string{"admin:repo_hook"}
84 | repoGranularScopes := []string{"write:repo_hook", "read:repo_hook"}
85 |
86 | scopes, err := GetScopes(ctx, client)
87 |
88 | if err != nil {
89 | return fmt.Errorf("failed to get scopes: %v", err)
90 | }
91 | if len(scopes) == 0 {
92 | return fmt.Errorf("permissions error: no scopes found for the github client")
93 | }
94 |
95 | if cfg.GitProviderConfig.OrgLevelWebhook {
96 | if utils.ListContains(orgScopes, scopes) {
97 | return nil
98 | }
99 | return fmt.Errorf("permissions error: %v is not a valid scope for the org level permissions", scopes)
100 | }
101 |
102 | if utils.ListContains(repoAdminScopes, scopes) {
103 | return nil
104 | }
105 | if utils.ListContains(repoGranularScopes, scopes) {
106 | return nil
107 | }
108 |
109 | return fmt.Errorf("permissions error: %v is not a valid scope for the repo level permissions", scopes)
110 | }
111 |
--------------------------------------------------------------------------------
/docs/getting_started/installation.md:
--------------------------------------------------------------------------------
1 | ## Instalation
2 |
3 | Piper should be deployed in the cluster with Argo Workflows.
4 | Piper will create a CRD that Argo Workflows will pick, so install or configure Piper to create those CRDs in the right namespace.
5 |
6 | Please check out [values.yaml](https://github.com/Rookout/piper/tree/main/helm-chart/values.yaml) file of the helm chart configurations.
7 |
8 | To add piper helm repo run:
9 | ```bash
10 | helm repo add piper https://piper.rookout.com
11 | ```
12 |
13 | After configuring Piper [values.yaml](https://github.com/Rookout/piper/tree/main/helm-chart/values.yaml), run the following command for installation:
14 | ```bash
15 | helm upgrade --install piper piper/piper \
16 | -f YOUR_VALUES_FILE.yaml
17 | ```
18 |
19 | ---
20 |
21 | ## Required Configuration
22 |
23 | ### Ingress
24 |
25 | Piper should listen to webhooks from your git provider.
26 | Expose it using ingress or service, then provide the address to `piper.webhook.url` as followed:
27 | `https://PIPER_EXPOESED_URL/webhook`
28 |
29 | Checkout [values.yaml](https://github.com/Rookout/piper/tree/main/helm-chart/values.yaml)
30 |
31 | ### Git
32 |
33 | Piper will use git for fetching `.workflows` folder and receiving events using webhooks.
34 |
35 | To pick which git provider you are using provide `gitProvider.name` configuration in helm chart (Now only supports GitHub and Bitbucket).
36 |
37 | Also configure you organization (Github) or workspace (Bitbucket) name using `gitProvider.organization.name` in helm chart.
38 |
39 | #### Git Token Permissions
40 |
41 | The token should have access for creating webhooks and read repositories content.
42 | For GitHub configure `admin:org` and `write:org` permissions in Classic Token.
43 | For Bitbucket configure `Repositories:read`, `Webhooks:read and write` and `Pull requests:read` permissions (for multiple repos use workspace token).
44 |
45 | #### Token
46 |
47 | The git token should be passed as secret in the helm chart at `gitProvider.token`.
48 | Can be passed as parameter in helm install command using `--set piper.gitProvider.token=YOUR_GIT_TOKEN`
49 |
50 | Alternatively, you can consume already existing secret and fill up `piper.gipProvider.existingSecret`.
51 | The key should be name `token`. Can be created using
52 | ```bash
53 | kubectl create secret generic piper-git-token --from-literal=token=YOUR_GIT_OKEN
54 | ```
55 |
56 | #### Webhook creation
57 |
58 | Piper will create a webhook configuration for you, for the whole organization or for each repo you configure.
59 |
60 | Configure `piper.webhook.url` the address of piper that exposed with ingress with `/webhook` postfix.
61 |
62 | For organization level configure: `gitProvider.webhook.orgLevel` to `true`.
63 |
64 | For granular repo webhook provide list of repos at: `gitProvider.webhook.repoList`.
65 |
66 | Piper implements graceful shutdown, it will delete all the webhooks when terminated.
67 |
68 | #### Status check
69 |
70 | Piper will handle status checks for you.
71 | It will notify the GitProvider for the status of Workflow for specific commit that triggered Piper.
72 | For linking provide valid URL of your Argo Workflows server address at: `argoWorkflows.server.address`
73 |
74 | ---
75 |
76 | ### Argo Workflow Server (On development)
77 |
78 | Piper will use REST API to communicate with Argo Workflows server for linting or for creation of workflows (ARGO_WORKFLOWS_CREATE_CRD). Please follow this [configuration](https://argoproj.github.io/argo-workflows/rest-api/).
79 |
80 | To lint the workflow before submitting it, please configure the internal address of Argo Workflows server (for example, `argo-server.workflows.svc.cluster.local`) in the field: `argoWorkflows.server.address`. Argo will need a [token](https://argoproj.github.io/argo-workflows/access-token/) to authenticate. please provide the secret in `argoWorkflows.server.token`, Better to pass as a references to a secret in the field `argoWorkflows.server.token`.
81 |
82 | #### Skip CRD Creation (On development)
83 |
84 | Piper can communicate directly to Argo Workflow using ARGO_WORKFLOWS_CREATE_CRD environment variable, if you want to skip the creation of CRD change `argoWorkflows.crdCreation` to `false`.
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rookout/piper
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/Rookout/GoSDK v0.1.45
7 | github.com/argoproj/argo-workflows/v3 v3.4.8
8 | github.com/emicklei/go-restful/v3 v3.8.0
9 | github.com/gin-gonic/gin v1.9.1
10 | github.com/google/go-cmp v0.5.9
11 | github.com/google/go-github/v52 v52.0.0
12 | github.com/kelseyhightower/envconfig v1.4.0
13 | github.com/ktrysmt/go-bitbucket v0.9.66
14 | github.com/stretchr/testify v1.8.4
15 | github.com/tidwall/gjson v1.16.0
16 | golang.org/x/net v0.17.0
17 | gopkg.in/yaml.v3 v3.0.1
18 | k8s.io/apimachinery v0.24.3
19 | k8s.io/client-go v0.24.3
20 | )
21 |
22 | require (
23 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
24 | github.com/bytedance/sonic v1.9.1 // indirect
25 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
26 | github.com/cloudflare/circl v1.3.3 // indirect
27 | github.com/davecgh/go-spew v1.1.1 // indirect
28 | github.com/fallais/logrus-lumberjack-hook v0.0.0-20210917073259-3227e1ab93b0 // indirect
29 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
30 | github.com/gin-contrib/sse v0.1.0 // indirect
31 | github.com/go-errors/errors v1.4.1 // indirect
32 | github.com/go-logr/logr v1.2.3 // indirect
33 | github.com/go-openapi/jsonpointer v0.19.6 // indirect
34 | github.com/go-openapi/jsonreference v0.20.2 // indirect
35 | github.com/go-openapi/swag v0.22.3 // indirect
36 | github.com/go-playground/locales v0.14.1 // indirect
37 | github.com/go-playground/universal-translator v0.18.1 // indirect
38 | github.com/go-playground/validator/v10 v10.14.0 // indirect
39 | github.com/goccy/go-json v0.10.2 // indirect
40 | github.com/gogo/protobuf v1.3.2 // indirect
41 | github.com/golang/protobuf v1.5.3 // indirect
42 | github.com/google/gnostic v0.5.7-v3refs // indirect
43 | github.com/google/go-querystring v1.1.0 // indirect
44 | github.com/google/gofuzz v1.2.0 // indirect
45 | github.com/google/uuid v1.3.0 // indirect
46 | github.com/gorilla/websocket v1.5.0 // indirect
47 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
48 | github.com/hashicorp/golang-lru v0.5.4 // indirect
49 | github.com/imdario/mergo v0.3.13 // indirect
50 | github.com/josharian/intern v1.0.0 // indirect
51 | github.com/json-iterator/go v1.1.12 // indirect
52 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
53 | github.com/kr/pretty v0.3.1 // indirect
54 | github.com/leodido/go-urn v1.2.4 // indirect
55 | github.com/mailru/easyjson v0.7.7 // indirect
56 | github.com/mattn/go-isatty v0.0.19 // indirect
57 | github.com/mitchellh/mapstructure v1.5.0 // indirect
58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
59 | github.com/modern-go/reflect2 v1.0.2 // indirect
60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
61 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
62 | github.com/pkg/errors v0.9.1 // indirect
63 | github.com/pmezard/go-difflib v1.0.0 // indirect
64 | github.com/sirupsen/logrus v1.9.2 // indirect
65 | github.com/spf13/pflag v1.0.5 // indirect
66 | github.com/tidwall/match v1.1.1 // indirect
67 | github.com/tidwall/pretty v1.2.0 // indirect
68 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
69 | github.com/ugorji/go/codec v1.2.11 // indirect
70 | github.com/yhirose/go-peg v0.0.0-20210804202551-de25d6753cf1 // indirect
71 | golang.org/x/arch v0.3.0 // indirect
72 | golang.org/x/crypto v0.14.0 // indirect
73 | golang.org/x/oauth2 v0.11.0 // indirect
74 | golang.org/x/sys v0.13.0 // indirect
75 | golang.org/x/term v0.13.0 // indirect
76 | golang.org/x/text v0.13.0 // indirect
77 | golang.org/x/time v0.3.0 // indirect
78 | google.golang.org/appengine v1.6.7 // indirect
79 | google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
80 | google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
81 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
82 | google.golang.org/grpc v1.56.3 // indirect
83 | google.golang.org/protobuf v1.31.0 // indirect
84 | gopkg.in/inf.v0 v0.9.1 // indirect
85 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
86 | gopkg.in/yaml.v2 v2.4.0 // indirect
87 | k8s.io/api v0.24.3 // indirect
88 | k8s.io/klog/v2 v2.60.1 // indirect
89 | k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
90 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
91 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
92 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
93 | sigs.k8s.io/yaml v1.3.0 // indirect
94 | )
95 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## How To Contribute
2 |
3 | We appreciate contributions from the community to make Piper even better. To contribute, follow the steps below:
4 |
5 | 1. Fork the Piper repository to your GitHub account.
6 | 2. Clone the forked repository to your local machine:
7 | ```bash
8 | git clone https://github.com/your-username/Piper.git
9 | ```
10 | 3. Create a new branch to work on your feature or bug fix:
11 | ```bash
12 | git checkout -b my-feature
13 | ```
14 | 4. Make your changes, following the coding guidelines outlined in this document.
15 | 5. Commit your changes with clear and descriptive commit messages and sign it:
16 | ```bash
17 | git commit -s -m "fix: Add new feature"
18 | ```
19 | * please make sure you commit as described in [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/)
20 | 6. Push your changes to your forked repository:
21 | ```bash
22 | git push origin my-feature
23 | ```
24 | 7. Open a [pull request](#pull-requests) against the main branch of the original Piper repository.
25 |
26 | ## Pull Requests
27 |
28 | We welcome and appreciate contributions from the community. If you have developed a new feature, improvement, or bug fix for Piper, follow these steps to submit a pull request:
29 |
30 | 1. Make sure you have forked the Piper repository and created a new branch for your changes. Checkout [How To Contribute](#How-to-contribute).
31 | 2. commit your changes and push them to your forked repository.
32 | 3. Go to the Piper repository on GitHub.
33 | 4. Click on the "New Pull Request" button.
34 | 5. Select your branch and provide a [descriptive title](#pull-request-nameing) and detailed description of your changes.
35 | 6. If your pull request relates to an open issue, reference the issue in the description using the GitHub issue syntax (e.g., Fixes #123).
36 | 7. Submit the pull request, and our team will review your changes. We appreciate your patience during the review process and may provide feedback or request further modifications.
37 |
38 | ### Pull Request Naming
39 |
40 | The name should follow conventional commit naming.
41 |
42 | ## Coding Guidelines
43 |
44 | To maintain a consistent codebase and ensure readability, we follow a set of coding guidelines in Piper. Please adhere to the following guidelines when making changes:
45 |
46 | * Follow the [Effective Go](https://go.dev/doc/effective_go) guide for Go code.
47 | * Follow the [Folder convention](https://github.com/golang-standards/project-layout) guide for Go code.
48 | * Write clear and concise comments to explain the code's functionality.
49 | * Use meaningful variable and function names.
50 | * Make sure your code is properly formatted and free of syntax errors.
51 | * Run tests locally.
52 | * Check that the feature documented.
53 | * Add new packages only if necessary and already existing one, can't be used.
54 | * Add tests for new features or modification.
55 |
56 | ## Helm Chart Development
57 |
58 | To make sure that the documentation is updated use [helm-docs](https://github.com/norwoodj/helm-docs) comment convention. The pipeline will execute `helm-docs` command and update the version of the chart.
59 |
60 | Also, please make sure to run those commands locally to debug the chart before merging:
61 |
62 | ```bash
63 | make helm
64 | ```
65 |
66 | ## Local deployment
67 |
68 | To make it easy to develop locally, please run the following
69 |
70 | Prerequisites :
71 | 1. install helm
72 | ```bash
73 | brew install helm
74 | ```
75 | 2. install kubectl
76 | ```bash
77 | brew install kubectl
78 | ```
79 | 3. isntall docker
80 |
81 | 4. install ngrok
82 | ```bash
83 | brew install ngrok
84 | ```
85 | 6. install 5
86 | ```bash
87 | brew install kind
88 | ```
89 |
90 | Deployment:
91 | 1. make sure docker are running.
92 | 2. create tunnel with ngrok using `make ngrok`, save the `Forwarding` address.
93 | 3. create `values.dev.yaml` file that contains subset of chart's `value.yaml` file. check [example of values file](https://github.com/Rookout/piper/tree/main/examples/template.values.dev.yaml) rename it to `values.dev.yaml` and put in root directory.
94 | 4. use `make deploy`. it will do the following:
95 | * deploy a local registry as container
96 | * deploy a kind cluster as container with configuration
97 | * deploy nginx reverse proxy in the kind cluster
98 | * deploy Piper with the local helm chart
99 | 5. validate using `curl localhost/piper/healthz`.
100 |
101 | ## Debugging
102 |
103 | For debugging the best practice is to use Rookout. To enable this function pass a Rookout token in the chart `rookout.token` or as existing secret `rookout.existingSecret`
--------------------------------------------------------------------------------
/pkg/utils/common.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "hash/fnv"
7 | "regexp"
8 | "strings"
9 |
10 | "gopkg.in/yaml.v3"
11 | "k8s.io/client-go/rest"
12 | "k8s.io/client-go/tools/clientcmd"
13 | )
14 |
15 | func ListContains(subList, list []string) bool {
16 | if len(subList) > len(list) {
17 | return false
18 | }
19 | for _, element := range subList {
20 | found := false
21 | for _, b := range list {
22 | if element == b {
23 | found = true
24 | break
25 | }
26 | }
27 | if !found {
28 | return false
29 | }
30 | }
31 | return true
32 | }
33 |
34 | func IsElementExists(list []string, element string) bool {
35 | for _, item := range list {
36 | if item == element {
37 | return true
38 | }
39 | }
40 | return false
41 | }
42 |
43 | func IsElementMatch(element string, elements []string) bool {
44 | if IsElementExists(elements, "*") {
45 | return true
46 | }
47 |
48 | return IsElementExists(elements, element)
49 | }
50 |
51 | func GetClientConfig(kubeConfig string) (*rest.Config, error) {
52 | if kubeConfig != "" {
53 | return clientcmd.BuildConfigFromFlags("", kubeConfig)
54 | }
55 | return rest.InClusterConfig()
56 | }
57 |
58 | func AddPrefixToList(list []string, prefix string) []string {
59 | result := make([]string, len(list))
60 |
61 | for i, item := range list {
62 | result[i] = prefix + item
63 | }
64 |
65 | return result
66 | }
67 |
68 | func StringToMap(str string) map[string]string {
69 | pairs := strings.Split(str, ",")
70 | m := make(map[string]string)
71 |
72 | for _, pair := range pairs {
73 | keyValue := strings.Split(pair, ":")
74 | if len(keyValue) == 2 {
75 | key := strings.TrimSpace(keyValue[0])
76 | value := strings.TrimSpace(keyValue[1])
77 | m[key] = value
78 | }
79 | }
80 |
81 | return m
82 | }
83 |
84 | func ConvertYAMLListToJSONList(yamlString string) ([]byte, error) {
85 | // Unmarshal YAML into a map[string]interface{}
86 | yamlData := make([]map[string]interface{}, 0)
87 | err := yaml.Unmarshal([]byte(yamlString), &yamlData)
88 | if err != nil {
89 | return nil, fmt.Errorf("failed to unmarshal YAML: %v", err)
90 | }
91 |
92 | // Marshal the YAML data as JSON
93 | jsonBytes, err := json.Marshal(&yamlData)
94 | if err != nil {
95 | return nil, fmt.Errorf("failed to marshal JSON: %v", err)
96 | }
97 |
98 | return jsonBytes, nil
99 | }
100 |
101 | func ConvertYAMLToJSON(yamlString []byte) ([]byte, error) {
102 | // Unmarshal YAML into a map[string]interface{}
103 | yamlData := make(map[string]interface{})
104 | err := yaml.Unmarshal(yamlString, &yamlData)
105 | if err != nil {
106 | return nil, fmt.Errorf("failed to unmarshal YAML: %v", err)
107 | }
108 |
109 | // Marshal the YAML data as JSON
110 | jsonBytes, err := json.Marshal(&yamlData)
111 | if err != nil {
112 | return nil, fmt.Errorf("failed to marshal JSON: %v", err)
113 | }
114 |
115 | return jsonBytes, nil
116 | }
117 |
118 | func SPtr(str string) *string {
119 | return &str
120 | }
121 |
122 | func BPtr(b bool) *bool {
123 | return &b
124 | }
125 |
126 | func IPtr(i int64) *int64 {
127 | return &i
128 | }
129 |
130 | func ValidateHTTPFormat(input string) bool {
131 | regex := `^(https?://)([\w-]+(\.[\w-]+)*)(:\d+)?(/[\w-./?%&=]*)?$`
132 | match, _ := regexp.MatchString(regex, input)
133 | return match
134 | }
135 |
136 | func TrimString(s string, maxLength int) string {
137 | if maxLength >= len(s) {
138 | return s
139 | }
140 | return s[:maxLength]
141 | }
142 |
143 | func StringToInt64(input string) int64 {
144 | h := fnv.New64a()
145 | h.Write([]byte(input))
146 | hashValue := h.Sum64()
147 |
148 | // Convert the hash value to int64
149 | int64Value := int64(hashValue)
150 | // Make sure the value is positive (int64 can represent only non-negative values)
151 | if int64Value < 0 {
152 | int64Value = int64Value * -1
153 | }
154 |
155 | return int64Value
156 | }
157 |
158 | func RemoveBraces(input string) string {
159 | output := strings.ReplaceAll(input, "{", "")
160 | output = strings.ReplaceAll(output, "}", "")
161 | return output
162 | }
163 |
164 | func ExtractStringsBetweenTags(input string) []string {
165 | re := regexp.MustCompile(`<([^>]+)>`)
166 | matches := re.FindAllStringSubmatch(input, -1)
167 |
168 | var result []string
169 | for _, match := range matches {
170 | result = append(result, match[1])
171 | }
172 |
173 | return result
174 | }
175 |
176 | func SanitizeString(input string) string {
177 | // Replace whitespace with "-"
178 | input = strings.ReplaceAll(input, " ", "-")
179 |
180 | // Define a regular expression pattern to match characters that are not a-z, A-Z, _, or .
181 | // Use a negated character class to allow a-z, A-Z, _, and .
182 | pattern := regexp.MustCompile(`[^a-zA-Z_1-9\.-]+`)
183 |
184 | // Remove characters that don't match the pattern
185 | sanitized := pattern.ReplaceAllString(input, "")
186 |
187 | return sanitized
188 | }
189 |
--------------------------------------------------------------------------------
/pkg/git_provider/bitbucket_test.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "github.com/ktrysmt/go-bitbucket"
8 | "github.com/rookout/piper/pkg/conf"
9 | "github.com/rookout/piper/pkg/utils"
10 | assertion "github.com/stretchr/testify/assert"
11 | "golang.org/x/net/context"
12 | "net/http"
13 | "testing"
14 | )
15 |
16 | func TestBitbucketListFiles(t *testing.T) {
17 | // Prepare
18 | client, mux, _, teardown := setupBitbucket()
19 | defer teardown()
20 |
21 | repoContent := &bitbucket.RepositoryFile{
22 | Type: "file",
23 | Path: ".workflows/exit.yaml",
24 | }
25 |
26 | repoContent2 := &bitbucket.RepositoryFile{
27 | Type: "file",
28 | Path: ".workflows/main.yaml",
29 | }
30 |
31 | data := map[string]interface{}{"values": []bitbucket.RepositoryFile{*repoContent, *repoContent2}}
32 | jsonBytes, _ := json.Marshal(data)
33 |
34 | mux.HandleFunc("/repositories/test/test-repo1/src/branch1/.workflows/", func(w http.ResponseWriter, r *http.Request) {
35 | testMethod(t, r, "GET")
36 | //testFormValues(t, r, values{})
37 |
38 | _, _ = fmt.Fprint(w, string(jsonBytes))
39 | })
40 |
41 | c := BitbucketClientImpl{
42 | client: client,
43 | cfg: &conf.GlobalConfig{
44 | GitProviderConfig: conf.GitProviderConfig{
45 | OrgLevelWebhook: false,
46 | OrgName: "test",
47 | RepoList: "test-repo1",
48 | },
49 | },
50 | }
51 | ctx := context.Background()
52 |
53 | // Execute
54 | actualContent, err := c.ListFiles(&ctx, "test-repo1", "branch1", ".workflows")
55 | expectedContent := []string{"exit.yaml", "main.yaml"}
56 |
57 | // Assert
58 | assert := assertion.New(t)
59 | assert.NotNil(t, err)
60 | assert.Equal(expectedContent, actualContent)
61 |
62 | }
63 |
64 | func TestBitbucketSetStatus(t *testing.T) {
65 | // Prepare
66 | ctx := context.Background()
67 | assert := assertion.New(t)
68 | client, mux, _, teardown := setupBitbucket()
69 | defer teardown()
70 |
71 | mux.HandleFunc("/repositories/test/test-repo1/commit/test-commit/statuses/build", func(w http.ResponseWriter, r *http.Request) {
72 | testMethod(t, r, "POST")
73 | testFormValues(t, r, values{})
74 |
75 | w.WriteHeader(http.StatusCreated)
76 | jsonBytes := []byte(`{"status": "ok"}`)
77 | _, _ = fmt.Fprint(w, string(jsonBytes))
78 | })
79 |
80 | c := BitbucketClientImpl{
81 | client: client,
82 | cfg: &conf.GlobalConfig{
83 | GitProviderConfig: conf.GitProviderConfig{
84 | Provider: "bitbucket",
85 | OrgLevelWebhook: false,
86 | OrgName: "test",
87 | RepoList: "test-repo1",
88 | },
89 | },
90 | }
91 |
92 | // Define test cases
93 | tests := []struct {
94 | name string
95 | repo *string
96 | commit *string
97 | linkURL *string
98 | status *string
99 | message *string
100 | wantedError error
101 | }{
102 | {
103 | name: "Notify success",
104 | repo: utils.SPtr("test-repo1"),
105 | commit: utils.SPtr("test-commit"),
106 | linkURL: utils.SPtr("https://argo"),
107 | status: utils.SPtr("success"),
108 | message: utils.SPtr(""),
109 | wantedError: nil,
110 | },
111 | {
112 | name: "Notify pending",
113 | repo: utils.SPtr("test-repo1"),
114 | commit: utils.SPtr("test-commit"),
115 | linkURL: utils.SPtr("https://argo"),
116 | status: utils.SPtr("pending"),
117 | message: utils.SPtr(""),
118 | wantedError: nil,
119 | },
120 | {
121 | name: "Notify error",
122 | repo: utils.SPtr("test-repo1"),
123 | commit: utils.SPtr("test-commit"),
124 | linkURL: utils.SPtr("https://argo"),
125 | status: utils.SPtr("error"),
126 | message: utils.SPtr("some message"),
127 | wantedError: nil,
128 | },
129 | {
130 | name: "Notify failure",
131 | repo: utils.SPtr("test-repo1"),
132 | commit: utils.SPtr("test-commit"),
133 | linkURL: utils.SPtr("https://argo"),
134 | status: utils.SPtr("failure"),
135 | message: utils.SPtr(""),
136 | wantedError: nil,
137 | },
138 | {
139 | name: "Non managed repo",
140 | repo: utils.SPtr("non-existing-repo"),
141 | commit: utils.SPtr("test-commit"),
142 | linkURL: utils.SPtr("https://argo"),
143 | status: utils.SPtr("error"),
144 | message: utils.SPtr(""),
145 | wantedError: errors.New("some error"),
146 | },
147 | {
148 | name: "Non existing commit",
149 | repo: utils.SPtr("test-repo1"),
150 | commit: utils.SPtr("not-exists"),
151 | linkURL: utils.SPtr("https://argo"),
152 | status: utils.SPtr("error"),
153 | message: utils.SPtr(""),
154 | wantedError: errors.New("some error"),
155 | },
156 | }
157 | // Run test cases
158 | for _, test := range tests {
159 | t.Run(test.name, func(t *testing.T) {
160 |
161 | // Call the function being tested
162 | err := c.SetStatus(&ctx, test.repo, test.commit, test.linkURL, test.status, test.message)
163 |
164 | // Use assert to check the equality of the error
165 | if test.wantedError != nil {
166 | assert.Error(err)
167 | assert.NotNil(err)
168 | } else {
169 | assert.NoError(err)
170 | assert.Nil(err)
171 | }
172 | })
173 | }
174 |
175 | }
176 |
--------------------------------------------------------------------------------
/pkg/workflow_handler/workflows_test.go:
--------------------------------------------------------------------------------
1 | package workflow_handler
2 |
3 | import (
4 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
5 | "github.com/rookout/piper/pkg/common"
6 | "github.com/rookout/piper/pkg/conf"
7 | "github.com/rookout/piper/pkg/git_provider"
8 | assertion "github.com/stretchr/testify/assert"
9 | "testing"
10 | )
11 |
12 | func TestSelectConfig(t *testing.T) {
13 | var wfc *conf.WorkflowsConfig
14 |
15 | assert := assertion.New(t)
16 | // Create a sample WorkflowsBatch object for testing
17 | configName := "default"
18 | workflowsBatch := &common.WorkflowsBatch{
19 | Config: &configName, // Set the desired config name here
20 | Payload: &git_provider.WebhookPayload{},
21 | }
22 |
23 | // Create a mock WorkflowsClientImpl object with necessary dependencies
24 | wfc = &conf.WorkflowsConfig{Configs: map[string]*conf.ConfigInstance{
25 | "default": {Spec: v1alpha1.WorkflowSpec{},
26 | OnExit: []v1alpha1.DAGTask{}},
27 | "config1": {Spec: v1alpha1.WorkflowSpec{},
28 | OnExit: []v1alpha1.DAGTask{}},
29 | }}
30 |
31 | wfcImpl := &WorkflowsClientImpl{
32 | cfg: &conf.GlobalConfig{
33 | WorkflowsConfig: *wfc,
34 | },
35 | }
36 |
37 | // Call the SelectConfig function
38 | returnConfigName, err := wfcImpl.SelectConfig(workflowsBatch)
39 |
40 | // Assert the expected output
41 | assert.Equal("default", returnConfigName)
42 | assert.Nil(err)
43 |
44 | // Test case 2
45 | configName = "config1"
46 | workflowsBatch = &common.WorkflowsBatch{
47 | Config: &configName, // Set the desired config name here
48 | Payload: &git_provider.WebhookPayload{},
49 | }
50 |
51 | // Call the SelectConfig function
52 | returnConfigName, err = wfcImpl.SelectConfig(workflowsBatch)
53 |
54 | // Assert the expected output
55 | assert.Equal("config1", returnConfigName)
56 | assert.Nil(err)
57 |
58 | // Test case 3 - selection of non-existing config when default config exists
59 | configName = "notInConfigs"
60 | workflowsBatch = &common.WorkflowsBatch{
61 | Config: &configName, // Set the desired config name here
62 | Payload: &git_provider.WebhookPayload{},
63 | }
64 |
65 | // Call the SelectConfig function
66 | returnConfigName, err = wfcImpl.SelectConfig(workflowsBatch)
67 |
68 | // Assert the expected output
69 | assert.Equal("default", returnConfigName)
70 | assert.NotNil(err)
71 |
72 | // Test case 4 - selection of non-existing config when default config not exists
73 | configName = "notInConfig"
74 | workflowsBatch = &common.WorkflowsBatch{
75 | Config: &configName, // Set the desired config name here
76 | Payload: &git_provider.WebhookPayload{},
77 | }
78 |
79 | wfc4 := &conf.WorkflowsConfig{Configs: map[string]*conf.ConfigInstance{
80 | "config1": {Spec: v1alpha1.WorkflowSpec{},
81 | OnExit: []v1alpha1.DAGTask{}},
82 | }}
83 |
84 | wfcImpl4 := &WorkflowsClientImpl{
85 | cfg: &conf.GlobalConfig{
86 | WorkflowsConfig: *wfc4,
87 | },
88 | }
89 |
90 | // Call the SelectConfig function
91 | returnConfigName, err = wfcImpl4.SelectConfig(workflowsBatch)
92 |
93 | // Assert the expected output
94 | assert.NotNil(returnConfigName)
95 | assert.NotNil(err)
96 | }
97 |
98 | func TestCreateWorkflow(t *testing.T) {
99 | var wfc *conf.WorkflowsConfig
100 | var wfs *conf.WorkflowServerConfig
101 |
102 | // Create a WorkflowsClientImpl instance
103 | assert := assertion.New(t)
104 | // Create a mock WorkflowsClientImpl object with necessary dependencies
105 | wfc = &conf.WorkflowsConfig{Configs: map[string]*conf.ConfigInstance{
106 | "default": {Spec: v1alpha1.WorkflowSpec{},
107 | OnExit: []v1alpha1.DAGTask{}},
108 | "config1": {Spec: v1alpha1.WorkflowSpec{},
109 | OnExit: []v1alpha1.DAGTask{}},
110 | }}
111 |
112 | wfs = &conf.WorkflowServerConfig{Namespace: "default"}
113 |
114 | wfcImpl := &WorkflowsClientImpl{
115 | cfg: &conf.GlobalConfig{
116 | WorkflowsConfig: *wfc,
117 | WorkflowServerConfig: *wfs,
118 | },
119 | }
120 |
121 | // Create a sample WorkflowSpec
122 | spec := &v1alpha1.WorkflowSpec{
123 | // Assign values to the fields of WorkflowSpec
124 | // ...
125 |
126 | // Example assignments:
127 | Entrypoint: "my-entrypoint",
128 | }
129 |
130 | // Create a sample WorkflowsBatch
131 | workflowsBatch := &common.WorkflowsBatch{
132 | Payload: &git_provider.WebhookPayload{
133 | Repo: "my-repo",
134 | Branch: "my-branch",
135 | User: "my-user",
136 | Commit: "my-commit",
137 | },
138 | }
139 |
140 | // Call the CreateWorkflow method
141 | workflow, err := wfcImpl.CreateWorkflow(spec, workflowsBatch)
142 |
143 | // Assert that no error occurred
144 | assert.NoError(err)
145 |
146 | // Assert that the returned workflow is not nil
147 | assert.NotNil(workflow)
148 |
149 | // Assert that the workflow's GenerateName, Namespace, and Labels are assigned correctly
150 | assert.Equal("my-repo-my-branch-", workflow.ObjectMeta.GenerateName)
151 | assert.Equal(wfcImpl.cfg.Namespace, workflow.ObjectMeta.Namespace)
152 | assert.Equal(map[string]string{
153 | "piper.rookout.com/notified": "false",
154 | "repo": "my-repo",
155 | "branch": "my-branch",
156 | "user": "my-user",
157 | "commit": "my-commit",
158 | }, workflow.ObjectMeta.Labels)
159 |
160 | // Assert that the workflow's Spec is assigned correctly
161 | assert.Equal(*spec, workflow.Spec)
162 | }
163 |
--------------------------------------------------------------------------------
/helm-chart/README.md:
--------------------------------------------------------------------------------
1 | # piper
2 |
3 |   
4 |
5 | A Helm chart for Piper
6 |
7 | ## Values
8 |
9 | | Key | Type | Default | Description |
10 | |-----|------|---------|-------------|
11 | | affinity | object | `{}` | Assign custom [affinity] rules to the deployment |
12 | | autoscaling.enabled | bool | `false` | Wheter to enable auto-scaling of piper. |
13 | | autoscaling.maxReplicas | int | `5` | Maximum reoplicas of Piper. |
14 | | autoscaling.minReplicas | int | `1` | Minimum reoplicas of Piper. |
15 | | autoscaling.targetCPUUtilizationPercentage | int | `85` | CPU utilization percentage threshold. |
16 | | autoscaling.targetMemoryUtilizationPercentage | int | `85` | Memory utilization percentage threshold. |
17 | | env | list | `[]` | Additional environment variables for Piper. A list of name/value maps. |
18 | | extraLabels | object | `{}` | Deployment and pods extra labels |
19 | | fullnameOverride | string | `""` | String to fully override "piper.fullname" template |
20 | | image.name | string | `"piper"` | Piper image name |
21 | | image.pullPolicy | string | `"IfNotPresent"` | Piper image pull policy |
22 | | image.repository | string | `"rookout"` | Piper public dockerhub repo |
23 | | image.tag | string | `""` | Piper image tag |
24 | | imagePullSecrets | list | `[]` | secret to use for image pulling |
25 | | ingress.annotations | object | `{}` | Piper ingress annotations |
26 | | ingress.className | string | `""` | Piper ingress class name |
27 | | ingress.enabled | bool | `false` | Enable Piper ingress support |
28 | | ingress.hosts | list | `[{"host":"piper.example.local","paths":[{"path":"/","pathType":"ImplementationSpecific"}]}]` | Piper ingress hosts # Hostnames must be provided if Ingress is enabled. |
29 | | ingress.tls | list | `[]` | Controller ingress tls |
30 | | lifecycle | object | `{}` | Specify postStart and preStop lifecycle hooks for Piper container |
31 | | nameOverride | string | `""` | String to partially override "piper.fullname" template |
32 | | nodeSelector | object | `{}` | [Node selector] |
33 | | piper.argoWorkflows.crdCreation | bool | `true` | Whether create Workflow CRD or send direct commands to Argo Workflows server. |
34 | | piper.argoWorkflows.server.address | string | `""` | The DNS address of Argo Workflow server that Piper can address. |
35 | | piper.argoWorkflows.server.existingSecret | string | `nil` | |
36 | | piper.argoWorkflows.server.namespace | string | `""` | The namespace in which the Workflow CRD will be created. |
37 | | piper.argoWorkflows.server.token | string | `""` | This will create a secret named -token and with the key 'token' |
38 | | piper.gitProvider.existingSecret | string | `nil` | |
39 | | piper.gitProvider.name | string | `"github"` | Name of your git provider (github/gitlab/bitbucket). for now, only github supported. |
40 | | piper.gitProvider.organization.name | string | `""` | Name of your Git Organization |
41 | | piper.gitProvider.token | string | `nil` | This will create a secret named -git-token and with the key 'token' |
42 | | piper.gitProvider.webhook.existingSecret | string | `nil` | |
43 | | piper.gitProvider.webhook.orgLevel | bool | `false` | Whether config webhook on org level |
44 | | piper.gitProvider.webhook.repoList | list | `[]` | Used of orgLevel=false, to configure webhook for each of the repos provided. |
45 | | piper.gitProvider.webhook.secret | string | `""` | This will create a secret named -webhook-secret and with the key 'secret' |
46 | | piper.gitProvider.webhook.url | string | `""` | The url in which piper listens for webhook, the path should be /webhook |
47 | | piper.workflowsConfig | object | `{}` | |
48 | | podAnnotations | object | `{}` | Annotations to be added to the Piper pods |
49 | | podSecurityContext | object | `{"fsGroup":1001,"runAsGroup":1001,"runAsUser":1001}` | Security Context to set on the pod level |
50 | | replicaCount | int | `1` | Piper number of replicas |
51 | | resources | object | `{"requests":{"cpu":"200m","memory":"512Mi"}}` | Resource limits and requests for the pods. |
52 | | rookout.existingSecret | string | `""` | |
53 | | rookout.token | string | `""` | Rookout token for agent configuration and enablement. |
54 | | securityContext | object | `{"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsNonRoot":true,"runAsUser":1001}` | Security Context to set on the container level |
55 | | service.annotations | object | `{}` | Piper service extra annotations |
56 | | service.labels | object | `{}` | Piper service extra labels |
57 | | service.port | int | `80` | Service port For TLS mode change the port to 443 |
58 | | service.type | string | `"ClusterIP"` | Sets the type of the Service |
59 | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
60 | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
61 | | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
62 | | tolerations | list | `[]` | [Tolerations] for use with node taints |
63 | | volumeMounts | list | `[]` | Volumes to mount to Piper container. |
64 | | volumes | list | `[]` | Volumes of Piper Pod. |
65 |
66 | ----------------------------------------------
67 | Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0)
68 |
--------------------------------------------------------------------------------
/pkg/webhook_creator/main.go:
--------------------------------------------------------------------------------
1 | package webhook_creator
2 |
3 | import (
4 | "fmt"
5 | "github.com/emicklei/go-restful/v3/log"
6 | "github.com/rookout/piper/pkg/clients"
7 | "github.com/rookout/piper/pkg/conf"
8 | "github.com/rookout/piper/pkg/git_provider"
9 | "github.com/rookout/piper/pkg/utils"
10 | "golang.org/x/net/context"
11 | "strconv"
12 | "strings"
13 | "sync"
14 | "time"
15 | )
16 |
17 | type WebhookCreatorImpl struct {
18 | clients *clients.Clients
19 | cfg *conf.GlobalConfig
20 | hooks map[int64]*git_provider.HookWithStatus
21 | mu sync.Mutex
22 | }
23 |
24 | func NewWebhookCreator(cfg *conf.GlobalConfig, clients *clients.Clients) *WebhookCreatorImpl {
25 | wr := &WebhookCreatorImpl{
26 | clients: clients,
27 | cfg: cfg,
28 | hooks: make(map[int64]*git_provider.HookWithStatus, 0),
29 | }
30 |
31 | return wr
32 | }
33 |
34 | func (wc *WebhookCreatorImpl) Start() {
35 |
36 | err := wc.initWebhooks()
37 | if err != nil {
38 | log.Print(err)
39 | panic("failed in initializing webhooks")
40 | }
41 | }
42 |
43 | func (wc *WebhookCreatorImpl) setWebhook(hookID int64, healthStatus bool, repoName string) {
44 | wc.mu.Lock()
45 | defer wc.mu.Unlock()
46 | wc.hooks[hookID] = &git_provider.HookWithStatus{HookID: hookID, HealthStatus: healthStatus, RepoName: &repoName}
47 | }
48 |
49 | func (wc *WebhookCreatorImpl) getWebhook(hookID int64) *git_provider.HookWithStatus {
50 | wc.mu.Lock()
51 | defer wc.mu.Unlock()
52 | hook, ok := wc.hooks[hookID]
53 | if !ok {
54 | return nil
55 | }
56 | return hook
57 | }
58 |
59 | func (wc *WebhookCreatorImpl) deleteWebhook(hookID int64) {
60 | wc.mu.Lock()
61 | defer wc.mu.Unlock()
62 |
63 | delete(wc.hooks, hookID)
64 | }
65 |
66 | func (wc *WebhookCreatorImpl) SetWebhookHealth(hookID int64, status bool) error {
67 |
68 | hook, ok := wc.hooks[hookID]
69 | if !ok {
70 | return fmt.Errorf("unable to find hookID: %d in internal hooks map %v", hookID, wc.hooks)
71 | }
72 | wc.setWebhook(hookID, status, *hook.RepoName)
73 | log.Printf("set health status to %s for hook id: %d", strconv.FormatBool(status), hookID)
74 | return nil
75 | }
76 |
77 | func (wc *WebhookCreatorImpl) setAllHooksHealth(status bool) {
78 | for hookID, hook := range wc.hooks {
79 | wc.setWebhook(hookID, status, *hook.RepoName)
80 | }
81 | log.Printf("set all hooks health status for to %s", strconv.FormatBool(status))
82 | }
83 |
84 | func (wc *WebhookCreatorImpl) initWebhooks() error {
85 |
86 | ctx := context.Background()
87 | if wc.cfg.GitProviderConfig.OrgLevelWebhook && len(wc.cfg.GitProviderConfig.RepoList) != 0 {
88 | return fmt.Errorf("org level webhook wanted but provided repositories list")
89 | }
90 | for _, repo := range strings.Split(wc.cfg.GitProviderConfig.RepoList, ",") {
91 | if wc.cfg.GitProviderConfig.Provider == "bitbucket" {
92 | repo = utils.SanitizeString(repo)
93 | }
94 | hook, err := wc.clients.GitProvider.SetWebhook(&ctx, &repo)
95 | if err != nil {
96 | return err
97 | }
98 | wc.setWebhook(hook.HookID, hook.HealthStatus, *hook.RepoName)
99 | }
100 |
101 | return nil
102 | }
103 |
104 | func (wc *WebhookCreatorImpl) Stop(ctx *context.Context) {
105 | if wc.cfg.GitProviderConfig.WebhookAutoCleanup {
106 | err := wc.deleteWebhooks(ctx)
107 | if err != nil {
108 | log.Printf("Failed to delete webhooks, error: %v", err)
109 | }
110 | }
111 | }
112 |
113 | func (wc *WebhookCreatorImpl) deleteWebhooks(ctx *context.Context) error {
114 |
115 | for hookID, hook := range wc.hooks {
116 | err := wc.clients.GitProvider.UnsetWebhook(ctx, hook)
117 | if err != nil {
118 | return err
119 | }
120 | wc.deleteWebhook(hookID)
121 | }
122 |
123 | return nil
124 | }
125 |
126 | func (wc *WebhookCreatorImpl) checkHooksHealth(timeoutSeconds time.Duration) bool {
127 | startTime := time.Now()
128 |
129 | for {
130 | allHealthy := true
131 | for _, hook := range wc.hooks {
132 | if !hook.HealthStatus {
133 | allHealthy = false
134 | break
135 | }
136 | }
137 |
138 | if allHealthy {
139 | return true
140 | }
141 |
142 | if time.Since(startTime) >= timeoutSeconds {
143 | break
144 | }
145 |
146 | time.Sleep(1 * time.Second) // Adjust the sleep duration as per your requirement
147 | }
148 |
149 | return false
150 | }
151 |
152 | func (wc *WebhookCreatorImpl) recoverHook(ctx *context.Context, hookID int64) error {
153 |
154 | log.Printf("started recover of hook %d", hookID)
155 | hook := wc.getWebhook(hookID)
156 | if hook == nil {
157 | return fmt.Errorf("failed to recover hook, hookID %d not found", hookID)
158 | }
159 | newHook, err := wc.clients.GitProvider.SetWebhook(ctx, hook.RepoName)
160 | if err != nil {
161 | return err
162 | }
163 | wc.deleteWebhook(hookID)
164 | wc.setWebhook(newHook.HookID, newHook.HealthStatus, *newHook.RepoName)
165 | log.Printf("successful recover of hook %d", hookID)
166 | return nil
167 |
168 | }
169 |
170 | func (wc *WebhookCreatorImpl) pingHooks(ctx *context.Context) error {
171 | for _, hook := range wc.hooks {
172 | err := wc.clients.GitProvider.PingHook(ctx, hook)
173 | if err != nil {
174 | return err
175 | }
176 | }
177 | return nil
178 | }
179 |
180 | func (wc *WebhookCreatorImpl) RunDiagnosis(ctx *context.Context) error {
181 | log.Printf("Starting webhook diagnostics")
182 | wc.setAllHooksHealth(false)
183 | err := wc.pingHooks(ctx)
184 | if err != nil {
185 | return err
186 | }
187 | if !wc.checkHooksHealth(5 * time.Second) {
188 | for hookID, hook := range wc.hooks {
189 | if !hook.HealthStatus {
190 | return fmt.Errorf("hook %d is not healthy", hookID)
191 | }
192 | }
193 | }
194 |
195 | log.Print("Successful webhook diagnosis")
196 | return nil
197 | }
198 |
--------------------------------------------------------------------------------
/helm-chart/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "piper.fullname" . }}
5 | labels:
6 | {{- include "piper.labels" . | nindent 4 }}
7 | spec:
8 | revisionHistoryLimit: 3
9 | {{- if not .Values.autoscaling.enabled }}
10 | replicas: {{ .Values.replicaCount }}
11 | {{- end }}
12 | selector:
13 | matchLabels:
14 | {{- include "piper.selectorLabels" . | nindent 6 }}
15 | template:
16 | metadata:
17 | {{- with .Values.podAnnotations }}
18 | annotations:
19 | {{- toYaml . | nindent 8 }}
20 | {{- end }}
21 | labels:
22 | {{- include "piper.selectorLabels" . | nindent 8 }}
23 | app: {{ .Chart.Name | trunc 63 | trimSuffix "-" }}
24 | version: {{ .Values.image.tag | default .Chart.AppVersion | trunc 63 | trimSuffix "-" }}
25 | spec:
26 | volumes:
27 | {{- if .Values.piper.workflowsConfig }}
28 | - name: piper-workflows-config
29 | configMap:
30 | name: piper-workflows-config
31 | {{- end }}
32 | {{- with .Values.volumes }}
33 | {{- toYaml . | nindent 8 }}
34 | {{- end }}
35 | {{- with .Values.imagePullSecrets }}
36 | imagePullSecrets:
37 | {{- toYaml . | nindent 8 }}
38 | {{- end }}
39 | serviceAccountName: {{ include "piper.serviceAccountName" . }}
40 | securityContext:
41 | {{- toYaml .Values.podSecurityContext | nindent 8 }}
42 | containers:
43 | - name: {{ .Chart.Name }}
44 | volumeMounts:
45 | {{- if .Values.piper.workflowsConfig }}
46 | - mountPath: /piper-config
47 | name: piper-workflows-config
48 | readOnly: true
49 | {{- end }}
50 | {{- with .Values.volumeMounts }}
51 | {{- toYaml . | nindent 12 }}
52 | {{- end }}
53 | securityContext:
54 | {{- toYaml .Values.securityContext | nindent 12 }}
55 | {{- with .Values.lifecycle }}
56 | lifecycle:
57 | {{- toYaml . | nindent 10 }}
58 | {{- end }}
59 | image: {{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag | default .Chart.AppVersion }}
60 | imagePullPolicy: {{ .Values.image.pullPolicy }}
61 | ports:
62 | - containerPort: 8080
63 | protocol: TCP
64 | livenessProbe:
65 | httpGet:
66 | path: /healthz
67 | port: 8080
68 | scheme: HTTP
69 | initialDelaySeconds: 10
70 | timeoutSeconds: 10
71 | periodSeconds: 60
72 | successThreshold: 1
73 | failureThreshold: 4
74 | readinessProbe:
75 | httpGet:
76 | path: /readyz
77 | port: 8080
78 | scheme: HTTP
79 | initialDelaySeconds: 2
80 | timeoutSeconds: 1
81 | periodSeconds: 4
82 | successThreshold: 1
83 | failureThreshold: 2
84 | resources:
85 | {{- toYaml .Values.resources | nindent 12 }}
86 | env:
87 | {{- if or .Values.rookout.token .Values.rookout.existingSecret }}
88 | - name: ROOKOUT_TOKEN
89 | valueFrom:
90 | secretKeyRef:
91 | name: {{ template "rookout.secretName" . }}
92 | key: token
93 | {{- end }}
94 | - name: GIT_PROVIDER
95 | value: {{ .Values.piper.gitProvider.name | quote }}
96 | - name: GIT_TOKEN
97 | valueFrom:
98 | secretKeyRef:
99 | name: {{ template "piper.gitProvider.tokenSecretName" . }}
100 | key: token
101 | - name: GIT_ORG_NAME
102 | value: {{ .Values.piper.gitProvider.organization.name | quote }}
103 | - name: GIT_WEBHOOK_URL
104 | value: {{ .Values.piper.gitProvider.webhook.url | quote }}
105 | - name: GIT_WEBHOOK_SECRET
106 | valueFrom:
107 | secretKeyRef:
108 | name: {{ template "piper.gitProvider.webhook.secretName" . }}
109 | key: secret
110 | - name: GIT_ORG_LEVEL_WEBHOOK
111 | value: {{ .Values.piper.gitProvider.webhook.orgLevel | quote }}
112 | - name: GIT_WEBHOOK_REPO_LIST
113 | value: {{ join "," .Values.piper.gitProvider.webhook.repoList | quote }}
114 | {{- if or .Values.piper.argoWorkflows.server.token .Values.piper.argoWorkflows.server.existingSecret }}
115 | - name: ARGO_WORKFLOWS_TOKEN
116 | valueFrom:
117 | secretKeyRef:
118 | name: {{ template "piper.argoWorkflows.tokenSecretName" . }}
119 | key: token
120 | {{- end }}
121 | - name: ARGO_WORKFLOWS_NAMESPACE
122 | value: {{ .Values.piper.argoWorkflows.server.namespace | default .Release.Namespace | quote }}
123 | - name: ARGO_WORKFLOWS_ADDRESS
124 | value: {{ .Values.piper.argoWorkflows.server.address | quote }}
125 | - name: ARGO_WORKFLOWS_CREATE_CRD
126 | value: {{ .Values.piper.argoWorkflows.crdCreation | quote }}
127 | {{- with .Values.env }}
128 | {{- toYaml . | nindent 10 }}
129 | {{- end }}
130 | {{- with .Values.nodeSelector }}
131 | nodeSelector:
132 | {{- toYaml . | nindent 8 }}
133 | {{- end }}
134 | {{- with .Values.affinity }}
135 | affinity:
136 | {{- toYaml . | nindent 8 }}
137 | {{- end }}
138 | {{- with .Values.tolerations }}
139 | tolerations:
140 | {{- toYaml . | nindent 8 }}
141 | {{- end }}
142 |
--------------------------------------------------------------------------------
/.github/workflows/e2e.yaml:
--------------------------------------------------------------------------------
1 | name: E2E tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | paths:
8 | - '**'
9 | - '!docs/**'
10 | pull_request:
11 | branches:
12 | - "main"
13 |
14 | concurrency:
15 | group: ${{ github.workflow }} # Bottleneck in ngrok tunnel, only one tunnel can exist for specific token -${{ github.ref }}
16 | # cancel-in-progress: true
17 |
18 | permissions:
19 | contents: read
20 |
21 | jobs:
22 | e2e-env-init:
23 | name: E2E Tests (on development)
24 | runs-on: ubuntu-latest
25 | timeout-minutes: 15
26 | steps:
27 | - uses: actions/checkout@v3
28 | - uses: docker/setup-qemu-action@v2
29 | - uses: docker/setup-buildx-action@v2
30 | with:
31 | driver-opts: network=host
32 | - uses: actions/setup-go@v4
33 | with:
34 | go-version: "1.20"
35 | cache: true
36 | - name: Install Ngrok Tunnel
37 | run: |
38 | curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null && \
39 | echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list && \
40 | sudo apt update && \
41 | sudo apt install ngrok
42 | touch ~/ngrok.log
43 | (timeout 30m ngrok http 80 --authtoken ${{ secrets.NGROK_AUTHTOKEN }} --log ~/ngrok.log) &
44 | echo $?
45 | - name: Install kind
46 | run: |
47 | curl -sSLo kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.19.0/kind-linux-amd64"
48 | chmod +x kind
49 | sudo mv kind /usr/local/bin/kind
50 | kind version
51 | - name: Install Kubectl
52 | run: |
53 | curl -sSLO "https://storage.googleapis.com/kubernetes-release/release/v1.26.1/bin/linux/amd64/kubectl"
54 | chmod +x kubectl
55 | sudo mv kubectl /usr/local/bin/kubectl
56 | kubectl version --client --output=yaml
57 | - name: Kubernetes KinD Cluster
58 | run: |
59 | make init-kind
60 | - name: install nginx
61 | run: |
62 | make init-nginx
63 | - name: install workflows
64 | run: |
65 | make init-argo-workflows
66 | - name: Build Docker Image
67 | uses: docker/build-push-action@v4
68 | with:
69 | context: .
70 | push: true
71 | tags: localhost:5001/piper:latest
72 | cache-from: type=gha
73 | cache-to: type=gha,mode=max
74 | - name: Check tunnel existence
75 | run: |
76 | echo "NGROK_URL=$(cat ~/ngrok.log | grep -o 'url=https://.*' | cut -d '=' -f 2)" >> $GITHUB_ENV
77 | cat ~/ngrok.log | grep -o 'url=https://.*' | cut -d '=' -f 2
78 | - name: init piper
79 | run: |
80 | helm upgrade --install piper ./helm-chart \
81 | -f ./examples/template.values.dev.yaml \
82 | --set piper.gitProvider.token="${{ secrets.GIT_TOKEN }}" \
83 | --set piper.gitProvider.webhook.url="${{ env.NGROK_URL }}/piper/webhook" \
84 | --set piper.gitProvider.webhook.repoList={piper-e2e-test} \
85 | --set piper.gitProvider.organization.name="rookout" \
86 | --set image.repository=localhost:5001 \
87 | --set piper.argoWorkflows.server.address="${{ env.NGROK_URL }}/argo" \
88 | --set-string env\[0\].name=GIT_WEBHOOK_AUTO_CLEANUP,env\[0\].value="true" && \
89 | sleep 20 && kubectl logs deployment/piper
90 | kubectl wait \
91 | --for=condition=ready pod \
92 | --selector=app=piper \
93 | --timeout=60s
94 | - uses: actions/checkout@v3
95 | with:
96 | repository: 'rookout/piper-e2e-test'
97 | path: piper-e2e-test
98 | ref: 'main'
99 | - name: inject some changes to piper-e2e-test repo
100 | run: |
101 | cd ./piper-e2e-test
102 | echo "" >> .workflows/triggers.yaml
103 | git config user.name 'e2e-test'
104 | git config user.email 'sonario@rookout.com'
105 | git commit -am "trigger e2e test"
106 | - name: Create Pull Request
107 | id: cpr
108 | uses: peter-evans/create-pull-request@v5
109 | with:
110 | token: ${{ secrets.GIT_TOKEN }}
111 | path: piper-e2e-test
112 | branch: ${{ github.head_ref }}-test
113 | title: ${{ github.head_ref }}-test
114 | delete-branch: true
115 | - name: Close Pull Request
116 | uses: peter-evans/close-pull@v3
117 | with:
118 | token: ${{ secrets.GIT_TOKEN }}
119 | pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
120 | repository: 'rookout/piper-e2e-test'
121 | comment: Auto-closing pull request
122 | delete-branch: true
123 | - name: Check Result
124 | run: |
125 | kubectl logs deployment/piper
126 | kubectl get workflows.argoproj.io -n workflows
127 | BRANCH_VALID_STRING=$(echo ${{ github.head_ref }}-test | tr '[:upper:]' '[:lower:]' | tr '_' '-' | tr -cd 'a-z0-9.\-')
128 |
129 | ## check if created
130 | RESULT=$(kubectl get workflows.argoproj.io -n workflows --selector=branch=$BRANCH_VALID_STRING --no-headers | grep piper-e2e-test)
131 | [ ! -z "$RESULT" ] && echo "CRD created $RESULT" || { echo "Workflow not exists, existing..."; exit 1; }
132 |
133 | ## check if status phase not Failed, if yes, show message
134 | RESULT=$(kubectl get workflows.argoproj.io -n workflows --selector=branch=$BRANCH_VALID_STRING --no-headers -o custom-columns="Status:status.phase")
135 | MESSAGE=$(kubectl get workflows.argoproj.io -n workflows --selector=branch=$BRANCH_VALID_STRING --no-headers -o custom-columns="Status:status.message")
136 | [ ! "$RESULT" == "Failed" ] && echo "CRD created $MESSAGE" || { echo "Workflow Failed $MESSAGE, existing..."; exit 1; }
--------------------------------------------------------------------------------
/pkg/event_handler/github_event_notifier_test.go:
--------------------------------------------------------------------------------
1 | package event_handler
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/rookout/piper/pkg/git_provider"
7 | assertion "github.com/stretchr/testify/assert"
8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9 | "net/http"
10 | "testing"
11 |
12 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
13 | "github.com/rookout/piper/pkg/clients"
14 | "github.com/rookout/piper/pkg/conf"
15 | )
16 |
17 | type mockGitProvider struct{}
18 |
19 | func (m *mockGitProvider) GetFile(ctx *context.Context, repo string, branch string, path string) (*git_provider.CommitFile, error) {
20 | return nil, nil
21 | }
22 |
23 | func (m *mockGitProvider) GetFiles(ctx *context.Context, repo string, branch string, paths []string) ([]*git_provider.CommitFile, error) {
24 | return nil, nil
25 | }
26 |
27 | func (m *mockGitProvider) ListFiles(ctx *context.Context, repo string, branch string, path string) ([]string, error) {
28 | return nil, nil
29 | }
30 |
31 | func (m *mockGitProvider) SetWebhook(ctx *context.Context, repo *string) (*git_provider.HookWithStatus, error) {
32 | return nil, nil
33 | }
34 |
35 | func (m *mockGitProvider) UnsetWebhook(ctx *context.Context, hook *git_provider.HookWithStatus) error {
36 | return nil
37 | }
38 |
39 | func (m *mockGitProvider) HandlePayload(ctx *context.Context, request *http.Request, secret []byte) (*git_provider.WebhookPayload, error) {
40 | return nil, nil
41 | }
42 |
43 | func (m *mockGitProvider) SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error {
44 | return nil
45 | }
46 |
47 | func (m *mockGitProvider) PingHook(ctx *context.Context, hook *git_provider.HookWithStatus) error {
48 | return nil
49 | }
50 |
51 | func (m *mockGitProvider) GetHooks() []*git_provider.HookWithStatus {
52 | return nil
53 | }
54 |
55 | func TestNotify(t *testing.T) {
56 | assert := assertion.New(t)
57 | ctx := context.Background()
58 |
59 | // Define test cases
60 | tests := []struct {
61 | name string
62 | workflow *v1alpha1.Workflow
63 | wantedError error
64 | }{
65 | {
66 | name: "Succeeded workflow",
67 | workflow: &v1alpha1.Workflow{
68 | ObjectMeta: metav1.ObjectMeta{
69 | Name: "test-workflow",
70 | Labels: map[string]string{
71 | "repo": "test-repo",
72 | "commit": "test-commit",
73 | },
74 | },
75 | Status: v1alpha1.WorkflowStatus{
76 | Phase: v1alpha1.WorkflowSucceeded,
77 | Message: "",
78 | },
79 | },
80 | wantedError: nil,
81 | },
82 | {
83 | name: "Failed workflow",
84 | workflow: &v1alpha1.Workflow{
85 | ObjectMeta: metav1.ObjectMeta{
86 | Name: "test-workflow",
87 | Labels: map[string]string{
88 | "repo": "test-repo",
89 | "commit": "test-commit",
90 | },
91 | },
92 | Status: v1alpha1.WorkflowStatus{
93 | Phase: v1alpha1.WorkflowFailed,
94 | Message: "something",
95 | },
96 | },
97 | wantedError: nil,
98 | },
99 | {
100 | name: "Error workflow",
101 | workflow: &v1alpha1.Workflow{
102 | ObjectMeta: metav1.ObjectMeta{
103 | Name: "test-workflow",
104 | Labels: map[string]string{
105 | "repo": "test-repo",
106 | "commit": "test-commit",
107 | },
108 | },
109 | Status: v1alpha1.WorkflowStatus{
110 | Phase: v1alpha1.WorkflowError,
111 | Message: "something",
112 | },
113 | },
114 | wantedError: nil,
115 | },
116 | {
117 | name: "Pending workflow",
118 | workflow: &v1alpha1.Workflow{
119 | ObjectMeta: metav1.ObjectMeta{
120 | Name: "test-workflow",
121 | Labels: map[string]string{
122 | "repo": "test-repo",
123 | "commit": "test-commit",
124 | },
125 | },
126 | Status: v1alpha1.WorkflowStatus{
127 | Phase: v1alpha1.WorkflowPending,
128 | Message: "something",
129 | },
130 | },
131 | wantedError: nil,
132 | },
133 | {
134 | name: "Running workflow",
135 | workflow: &v1alpha1.Workflow{
136 | ObjectMeta: metav1.ObjectMeta{
137 | Name: "test-workflow",
138 | Labels: map[string]string{
139 | "repo": "test-repo",
140 | "commit": "test-commit",
141 | },
142 | },
143 | Status: v1alpha1.WorkflowStatus{
144 | Phase: v1alpha1.WorkflowRunning,
145 | Message: "something",
146 | },
147 | },
148 | wantedError: nil,
149 | },
150 | {
151 | name: "Missing label repo",
152 | workflow: &v1alpha1.Workflow{
153 | ObjectMeta: metav1.ObjectMeta{
154 | Name: "test-workflow",
155 | Labels: map[string]string{
156 | "commit": "test-commit",
157 | },
158 | },
159 | Status: v1alpha1.WorkflowStatus{
160 | Phase: v1alpha1.WorkflowSucceeded,
161 | Message: "something",
162 | },
163 | },
164 | wantedError: errors.New("some error"),
165 | },
166 | {
167 | name: "Missing label commit",
168 | workflow: &v1alpha1.Workflow{
169 | ObjectMeta: metav1.ObjectMeta{
170 | Name: "test-workflow",
171 | Labels: map[string]string{
172 | "repo": "test-repo",
173 | },
174 | },
175 | Status: v1alpha1.WorkflowStatus{
176 | Phase: v1alpha1.WorkflowSucceeded,
177 | Message: "something",
178 | },
179 | },
180 | wantedError: errors.New("some error"),
181 | },
182 | }
183 |
184 | // Create a mock configuration and clients
185 | cfg := &conf.GlobalConfig{
186 | GitProviderConfig: conf.GitProviderConfig{Provider: "github"},
187 | WorkflowServerConfig: conf.WorkflowServerConfig{
188 | ArgoAddress: "http://workflow-server",
189 | Namespace: "test-namespace",
190 | },
191 | }
192 | globalClients := &clients.Clients{
193 | GitProvider: &mockGitProvider{},
194 | }
195 |
196 | // Create a new githubNotifier instance
197 | gn := NewGithubEventNotifier(cfg, globalClients)
198 |
199 | // Call the Notify method
200 |
201 | // Run test cases
202 | for _, test := range tests {
203 | t.Run(test.name, func(t *testing.T) {
204 | // Call the function being tested
205 | err := gn.Notify(&ctx, test.workflow)
206 |
207 | // Use assert to check the equality of the error
208 | if test.wantedError != nil {
209 | assert.Error(err)
210 | assert.NotNil(err)
211 | } else {
212 | assert.NoError(err)
213 | assert.Nil(err)
214 | }
215 | })
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/pkg/workflow_handler/workflows_utils_test.go:
--------------------------------------------------------------------------------
1 | package workflow_handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
6 | "github.com/rookout/piper/pkg/git_provider"
7 | assertion "github.com/stretchr/testify/assert"
8 | "testing"
9 | )
10 |
11 | func TestAddFilesToTemplates(t *testing.T) {
12 | assert := assertion.New(t)
13 |
14 | template := make([]v1alpha1.Template, 0)
15 | files := make([]*git_provider.CommitFile, 0)
16 |
17 | content := `
18 | - name: local-step
19 | inputs:
20 | parameters:
21 | - name: message
22 | script:
23 | image: alpine
24 | command: [ sh ]
25 | source: |
26 | echo "wellcome to {{ workflow.parameters.global }}
27 | echo "{{ inputs.parameters.message }}"
28 | `
29 | path := "path"
30 | files = append(files, &git_provider.CommitFile{
31 | Content: &content,
32 | Path: &path,
33 | })
34 |
35 | template, err := AddFilesToTemplates(template, files)
36 |
37 | assert.Nil(err)
38 | assert.Equal("alpine", template[0].Script.Container.Image)
39 | assert.Equal([]string{"sh"}, template[0].Script.Command)
40 | expectedScript := "echo \"wellcome to {{ workflow.parameters.global }}\necho \"{{ inputs.parameters.message }}\"\n"
41 | assert.Equal(expectedScript, template[0].Script.Source)
42 | }
43 | func TestValidateDAGTasks(t *testing.T) {
44 | assert := assertion.New(t)
45 | // Define test cases
46 | tests := []struct {
47 | name string
48 | tasks []v1alpha1.DAGTask
49 | want error
50 | }{
51 | {
52 | name: "Valid tasks",
53 | tasks: []v1alpha1.DAGTask{
54 | {Name: "Task1", Template: "Template1"},
55 | {Name: "Task2", TemplateRef: &v1alpha1.TemplateRef{Name: "Template2"}},
56 | },
57 | want: nil,
58 | },
59 | {
60 | name: "Empty task name",
61 | tasks: []v1alpha1.DAGTask{
62 | {Name: "", Template: "Template1"},
63 | },
64 | want: fmt.Errorf("task name cannot be empty"),
65 | },
66 | {
67 | name: "Empty template and templateRef",
68 | tasks: []v1alpha1.DAGTask{
69 | {Name: "Task1"},
70 | },
71 | want: fmt.Errorf("task template or templateRef cannot be empty"),
72 | },
73 | }
74 |
75 | // Run test cases
76 | for _, test := range tests {
77 | t.Run(test.name, func(t *testing.T) {
78 | // Call the function being tested
79 | got := ValidateDAGTasks(test.tasks)
80 |
81 | // Use assert to check the equality of the error
82 | if test.want != nil {
83 | assert.Error(got)
84 | assert.NotNil(got)
85 | } else {
86 | assert.NoError(got)
87 | assert.Nil(got)
88 | }
89 | })
90 | }
91 | }
92 |
93 | func TestCreateDAGTemplate(t *testing.T) {
94 | assert := assertion.New(t)
95 | t.Run("Empty file list", func(t *testing.T) {
96 | fileList := []*git_provider.CommitFile{}
97 | name := "template1"
98 | template, err := CreateDAGTemplate(fileList, name)
99 | assert.Nil(template)
100 | assert.Nil(err)
101 | })
102 |
103 | t.Run("Missing content or path", func(t *testing.T) {
104 | file := &git_provider.CommitFile{
105 | Content: nil,
106 | Path: nil,
107 | }
108 | fileList := []*git_provider.CommitFile{file}
109 | name := "template2"
110 | template, err := CreateDAGTemplate(fileList, name)
111 | assert.Nil(template)
112 | assert.NotNil(err)
113 | })
114 |
115 | t.Run("Valid file list", func(t *testing.T) {
116 | path3 := "some-path"
117 | content3 := `- name: local-step1
118 | template: local-step
119 | arguments:
120 | parameters:
121 | - name: message
122 | value: step-1
123 | - name: local-step2
124 | templateRef:
125 | name: common-toolkit
126 | template: versioning
127 | clusterScope: true
128 | arguments:
129 | parameters:
130 | - name: message
131 | value: step-2
132 | dependencies:
133 | - local-step1`
134 | file := &git_provider.CommitFile{
135 | Content: &content3,
136 | Path: &path3,
137 | }
138 | fileList := []*git_provider.CommitFile{file}
139 | name := "template3"
140 | template, err := CreateDAGTemplate(fileList, name)
141 |
142 | assert.Nil(err)
143 | assert.NotNil(template)
144 |
145 | assert.Equal(name, template.Name)
146 | assert.NotNil(template.DAG)
147 | assert.Equal(2, len(template.DAG.Tasks))
148 |
149 | assert.NotNil(template.DAG.Tasks[0])
150 | assert.Equal("local-step1", template.DAG.Tasks[0].Name)
151 | assert.Equal("local-step", template.DAG.Tasks[0].Template)
152 | assert.Equal(1, len(template.DAG.Tasks[0].Arguments.Parameters))
153 | assert.Equal("message", template.DAG.Tasks[0].Arguments.Parameters[0].Name)
154 | assert.Equal("step-1", template.DAG.Tasks[0].Arguments.Parameters[0].Value.String())
155 |
156 | assert.NotNil(template.DAG.Tasks[1])
157 | assert.Equal("local-step2", template.DAG.Tasks[1].Name)
158 | assert.Equal(1, len(template.DAG.Tasks[1].Dependencies))
159 | assert.Equal("local-step1", template.DAG.Tasks[1].Dependencies[0])
160 | assert.NotNil(template.DAG.Tasks[1].TemplateRef)
161 | assert.Equal("common-toolkit", template.DAG.Tasks[1].TemplateRef.Name)
162 | assert.Equal("versioning", template.DAG.Tasks[1].TemplateRef.Template)
163 | assert.True(template.DAG.Tasks[1].TemplateRef.ClusterScope)
164 | })
165 |
166 | t.Run("Invalid configuration", func(t *testing.T) {
167 | path4 := "some-path"
168 | content4 := `- noName: local-step1
169 | wrongkey2: local-step
170 | - noName: local-step2
171 | wrongkey: something
172 | dependencies:
173 | - local-step1`
174 | file := &git_provider.CommitFile{
175 | Content: &content4,
176 | Path: &path4,
177 | }
178 | fileList := []*git_provider.CommitFile{file}
179 | name := "template4"
180 | template, err := CreateDAGTemplate(fileList, name)
181 |
182 | assert.Nil(template)
183 | assert.NotNil(err)
184 | })
185 |
186 | t.Run("YAML syntax error", func(t *testing.T) {
187 | path5 := "some-path"
188 | content5 := `- noName: local-step1
189 | wrongkey2: local-step
190 | error: should be list`
191 | file := &git_provider.CommitFile{
192 | Content: &content5,
193 | Path: &path5,
194 | }
195 | fileList := []*git_provider.CommitFile{file}
196 | name := "template5"
197 | template, err := CreateDAGTemplate(fileList, name)
198 |
199 | assert.Nil(template)
200 | assert.NotNil(err)
201 | })
202 | }
203 |
204 | func TestConvertToValidString(t *testing.T) {
205 | assert := assertion.New(t)
206 |
207 | tests := []struct {
208 | input string
209 | expected string
210 | }{
211 | {"A@bC!-123.def", "abc-123.def"},
212 | {"Hello World!", "helloworld"},
213 | {"123$%^", "123"},
214 | {"abc_123.xyz", "abc-123.xyz"}, // Underscore (_) should be converted to hyphen (-)
215 | {"..--..", "..--.."}, // Only dots (.) and hyphens (-) should remain
216 | }
217 |
218 | for _, test := range tests {
219 | t.Run(test.input, func(t *testing.T) {
220 | converted := ConvertToValidString(test.input)
221 | assert.Equal(converted, test.expected)
222 | })
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/pkg/webhook_handler/webhook_handler.go:
--------------------------------------------------------------------------------
1 | package webhook_handler
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/rookout/piper/pkg/clients"
7 | "github.com/rookout/piper/pkg/common"
8 | "github.com/rookout/piper/pkg/conf"
9 | "github.com/rookout/piper/pkg/git_provider"
10 | "github.com/rookout/piper/pkg/utils"
11 | "gopkg.in/yaml.v3"
12 | "log"
13 | )
14 |
15 | type WebhookHandlerImpl struct {
16 | cfg *conf.GlobalConfig
17 | clients *clients.Clients
18 | Triggers *[]Trigger
19 | Payload *git_provider.WebhookPayload
20 | }
21 |
22 | func NewWebhookHandler(cfg *conf.GlobalConfig, clients *clients.Clients, payload *git_provider.WebhookPayload) (*WebhookHandlerImpl, error) {
23 | var err error
24 |
25 | return &WebhookHandlerImpl{
26 | cfg: cfg,
27 | clients: clients,
28 | Triggers: &[]Trigger{},
29 | Payload: payload,
30 | }, err
31 | }
32 |
33 | func (wh *WebhookHandlerImpl) RegisterTriggers(ctx *context.Context) error {
34 | if !IsFileExists(ctx, wh, "", ".workflows") {
35 | return fmt.Errorf(".workflows folder does not exist in %s/%s", wh.Payload.Repo, wh.Payload.Branch)
36 | }
37 |
38 | if !IsFileExists(ctx, wh, ".workflows", "triggers.yaml") {
39 | return fmt.Errorf(".workflows/triggers.yaml file does not exist in %s/%s", wh.Payload.Repo, wh.Payload.Branch)
40 | }
41 |
42 | triggers, err := wh.clients.GitProvider.GetFile(ctx, wh.Payload.Repo, wh.Payload.Branch, ".workflows/triggers.yaml")
43 | if err != nil {
44 | return fmt.Errorf("failed to get triggers content: %v", err)
45 | }
46 |
47 | log.Printf("triggers content is: \n %s \n", *triggers.Content) // DEBUG
48 |
49 | err = yaml.Unmarshal([]byte(*triggers.Content), wh.Triggers)
50 | if err != nil {
51 | return fmt.Errorf("failed to unmarshal triggers content: %v", err)
52 | }
53 | return nil
54 | }
55 |
56 | func (wh *WebhookHandlerImpl) PrepareBatchForMatchingTriggers(ctx *context.Context) ([]*common.WorkflowsBatch, error) {
57 | triggered := false
58 | var workflowBatches []*common.WorkflowsBatch
59 | for _, trigger := range *wh.Triggers {
60 | if trigger.Branches == nil {
61 | return nil, fmt.Errorf("trigger from repo %s branch %s missing branch field", wh.Payload.Repo, wh.Payload.Branch)
62 | }
63 | if trigger.Events == nil {
64 | return nil, fmt.Errorf("trigger from repo %s branch %s missing event field", wh.Payload.Repo, wh.Payload.Branch)
65 | }
66 |
67 | eventToCheck := wh.Payload.Event
68 | if wh.Payload.Action != "" {
69 | eventToCheck += "." + wh.Payload.Action
70 | }
71 | if utils.IsElementMatch(wh.Payload.Branch, *trigger.Branches) && utils.IsElementMatch(eventToCheck, *trigger.Events) {
72 | log.Printf(
73 | "Triggering event %s for repo %s branch %s are triggered.",
74 | wh.Payload.Event,
75 | wh.Payload.Repo,
76 | wh.Payload.Branch,
77 | )
78 | triggered = true
79 | onStartFiles, err := wh.clients.GitProvider.GetFiles(
80 | ctx,
81 | wh.Payload.Repo,
82 | wh.Payload.Branch,
83 | utils.AddPrefixToList(*trigger.OnStart, ".workflows/"),
84 | )
85 | if len(onStartFiles) == 0 {
86 | return nil, fmt.Errorf("one or more of onStart: %s files found in repo: %s branch %s", *trigger.OnStart, wh.Payload.Repo, wh.Payload.Branch)
87 | }
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | onExitFiles := make([]*git_provider.CommitFile, 0)
93 | if trigger.OnExit != nil {
94 | onExitFiles, err = wh.clients.GitProvider.GetFiles(
95 | ctx,
96 | wh.Payload.Repo,
97 | wh.Payload.Branch,
98 | utils.AddPrefixToList(*trigger.OnExit, ".workflows/"),
99 | )
100 | if len(onExitFiles) == 0 {
101 | log.Printf("one or more of onExist: %s files not found in repo: %s branch %s", *trigger.OnExit, wh.Payload.Repo, wh.Payload.Branch)
102 | }
103 | if err != nil {
104 | return nil, err
105 | }
106 | }
107 |
108 | templatesFiles := make([]*git_provider.CommitFile, 0)
109 | if trigger.Templates != nil {
110 | templatesFiles, err = wh.clients.GitProvider.GetFiles(
111 | ctx,
112 | wh.Payload.Repo,
113 | wh.Payload.Branch,
114 | utils.AddPrefixToList(*trigger.Templates, ".workflows/"),
115 | )
116 | if len(templatesFiles) == 0 {
117 | log.Printf("one or more of templates: %s files not found in repo: %s branch %s", *trigger.Templates, wh.Payload.Repo, wh.Payload.Branch)
118 | }
119 | if err != nil {
120 | return nil, err
121 | }
122 | }
123 |
124 | parameters := &git_provider.CommitFile{
125 | Path: nil,
126 | Content: nil,
127 | }
128 | if IsFileExists(ctx, wh, ".workflows", "parameters.yaml") {
129 | parameters, err = wh.clients.GitProvider.GetFile(
130 | ctx,
131 | wh.Payload.Repo,
132 | wh.Payload.Branch,
133 | ".workflows/parameters.yaml",
134 | )
135 | if err != nil {
136 | return nil, err
137 | }
138 | } else {
139 | log.Printf("parameters.yaml not found in repo: %s branch %s", wh.Payload.Repo, wh.Payload.Branch)
140 | }
141 |
142 | workflowBatches = append(workflowBatches, &common.WorkflowsBatch{
143 | OnStart: onStartFiles,
144 | OnExit: onExitFiles,
145 | Templates: templatesFiles,
146 | Parameters: parameters,
147 | Config: &trigger.Config,
148 | Payload: wh.Payload,
149 | })
150 | }
151 | }
152 | if !triggered {
153 | return nil, fmt.Errorf("no matching trigger found for event: %s action: %s in branch :%s", wh.Payload.Event, wh.Payload.Action, wh.Payload.Branch)
154 | }
155 | return workflowBatches, nil
156 | }
157 |
158 | func IsFileExists(ctx *context.Context, wh *WebhookHandlerImpl, path string, file string) bool {
159 | files, err := wh.clients.GitProvider.ListFiles(ctx, wh.Payload.Repo, wh.Payload.Branch, path)
160 | if err != nil {
161 | log.Printf("Error listing files in repo: %s branch: %s. %v", wh.Payload.Repo, wh.Payload.Branch, err)
162 | return false
163 | }
164 | if len(files) == 0 {
165 | log.Printf("Empty list of files in repo: %s branch: %s", wh.Payload.Repo, wh.Payload.Branch)
166 | return false
167 | }
168 |
169 | if utils.IsElementExists(files, file) {
170 | return true
171 | }
172 |
173 | return false
174 | }
175 |
176 | func HandleWebhook(ctx *context.Context, wh *WebhookHandlerImpl) ([]*common.WorkflowsBatch, error) {
177 | err := wh.RegisterTriggers(ctx)
178 | if err != nil {
179 | return nil, fmt.Errorf("failed to register triggers, error: %v", err)
180 | } else {
181 | log.Printf("successfully registered triggers for repo: %s branch: %s", wh.Payload.Repo, wh.Payload.Branch)
182 | }
183 |
184 | workflowsBatches, err := wh.PrepareBatchForMatchingTriggers(ctx)
185 | if err != nil {
186 | return nil, fmt.Errorf("failed to prepare matching triggers, error: %v", err)
187 | }
188 |
189 | if len(workflowsBatches) == 0 {
190 | log.Printf("no workflows to execute")
191 | return nil, fmt.Errorf("no workflows to execute for repo: %s branch: %s",
192 | wh.Payload.Repo,
193 | wh.Payload.Branch,
194 | )
195 | }
196 | return workflowsBatches, nil
197 | }
198 |
--------------------------------------------------------------------------------
/helm-chart/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for Piper.
2 | # For more information head to https:/github.com/rookout/piper
3 |
4 | # Map of Piper configurations.
5 | piper:
6 | gitProvider:
7 | # -- Name of your git provider (github/bitbucket).
8 | name: github
9 | # -- The token for authentication with the Git provider.
10 | # -- This will create a secret named -git-token and with the key 'token'
11 | token:
12 | # -- The token for authentication with the Git provider.
13 | # -- Reference to existing token with 'token' key.
14 | # -- can be created with `kubectl create secret generic piper-git-token --from-literal=token=YOUR_TOKEN`
15 | existingSecret: #piper-git-token
16 | # Map of organization configurations.
17 | organization:
18 | # -- Name of your Git Organization (GitHub) or Workspace (Bitbucket)
19 | name: ""
20 | # Map of webhook configurations.
21 | webhook:
22 | # -- The secret that will be used for webhook authentication
23 | # -- If not provided, will be generated
24 | # -- This will create a secret named -webhook-secret and with the key 'secret'
25 | secret: ""
26 | # -- The secret for webhook encryption
27 | # -- Reference to existing token with 'secret' key.
28 | # -- can be created with `kubectl create secret generic piper-webhook-secret --from-literal=secret=YOUR_TOKEN`
29 | existingSecret: #piper-webhook-secret
30 | # -- The url in which piper listens for webhook, the path should be /webhook
31 | url: "" #https://piper.example.local/webhook
32 | # -- Whether config webhook on org level (GitHub) or at workspace level (Bitbucket - not supported yet)
33 | orgLevel: false
34 | # -- (Github) Used of orgLevel=false, to configure webhook for each of the repos provided.
35 | repoList: []
36 |
37 | # Map of Argo Workflows configurations.
38 | argoWorkflows:
39 | # Map of Argo Workflows server configurations.
40 | server:
41 | # -- The namespace in which the Workflow CRD will be created.
42 | namespace: ""
43 | # -- The DNS address of Argo Workflow server that Piper can address.
44 | address: ""
45 | # -- The token for authentication with Argo Workflows server.
46 | # -- This will create a secret named -token and with the key 'token'
47 | token: ""
48 | # -- The token for authentication with Argo Workflows server.
49 | # -- Reference to existing token with 'token' key.
50 | # -- can be created with `kubectl create secret generic piper-argo-token --from-literal=token=YOUR_TOKEN`
51 | existingSecret: #piper-argo-token
52 | # -- Whether create Workflow CRD or send direct commands to Argo Workflows server.
53 | crdCreation: true
54 |
55 | workflowsConfig: {}
56 | # default: |
57 | # spec:
58 | # volumes:
59 | # - name: shared-volume
60 | # emptyDir: {}
61 | # serviceAccountName: argo-wf
62 | # activeDeadlineSeconds: 7200 # (seconds) == 2 hours
63 | # ttlStrategy:
64 | # secondsAfterCompletion: 28800 # (seconds) == 8 hours
65 | # podGC:
66 | # strategy: OnPodSuccess
67 | # archiveLogs: true
68 | # artifactRepositoryRef:
69 | # configMap: artifact-repositories
70 | # nodeSelector:
71 | # node_pool: workflows
72 | # tolerations:
73 | # - effect: NoSchedule
74 | # key: node_pool
75 | # operator: Equal
76 | # value: workflows
77 | # onExit: # optinal, will be overwritten if specifc in .wokrflows/exit.yaml.
78 | # - name: github-status
79 | # template: exit-handler
80 | # arguments:
81 | # parameters:
82 | # - name: param1
83 | # value: "{{ workflow.labels.repo }}"
84 |
85 | rookout:
86 | # -- Rookout token for agent configuration and enablement.
87 | token: ""
88 | # -- The token for Rookout.
89 | # -- Reference to existing token with 'token' key.
90 | # -- can be created with `kubectl create secret generic piper-rookout-token --from-literal=token=YOUR_TOKEN`
91 | existingSecret: ""
92 |
93 | # -- Piper number of replicas
94 | replicaCount: 1
95 |
96 | image:
97 | # -- Piper image name
98 | name: piper
99 | # -- Piper public dockerhub repo
100 | repository: rookout
101 | # -- Piper image pull policy
102 | pullPolicy: IfNotPresent
103 | # -- Piper image tag
104 | tag: ""
105 |
106 | # -- secret to use for image pulling
107 | imagePullSecrets: []
108 |
109 | # -- String to partially override "piper.fullname" template
110 | nameOverride: ""
111 |
112 | # -- String to fully override "piper.fullname" template
113 | fullnameOverride: ""
114 |
115 | serviceAccount:
116 | # -- Specifies whether a service account should be created
117 | create: true
118 | # -- Annotations to add to the service account
119 | annotations: {}
120 | # -- The name of the service account to use.
121 | # If not set and create is true, a name is generated using the fullname template
122 | name: ""
123 |
124 | # -- Annotations to be added to the Piper pods
125 | podAnnotations: {}
126 |
127 | # -- Security Context to set on the pod level
128 | podSecurityContext:
129 | fsGroup: 1001
130 | runAsUser: 1001
131 | runAsGroup: 1001
132 |
133 | # -- Security Context to set on the container level
134 | securityContext:
135 | runAsUser: 1001
136 | capabilities:
137 | drop:
138 | - ALL
139 | readOnlyRootFilesystem: true
140 | runAsNonRoot: true
141 |
142 |
143 | service:
144 | # -- Sets the type of the Service
145 | type: ClusterIP
146 | # -- Service port
147 | # For TLS mode change the port to 443
148 | port: 80
149 | #nodePort:
150 | # -- Piper service extra labels
151 | labels: {}
152 | # -- Piper service extra annotations
153 | annotations: {}
154 |
155 | ingress:
156 | # -- Enable Piper ingress support
157 | enabled: false
158 | # -- Piper ingress class name
159 | className: ""
160 | # -- Piper ingress annotations
161 | annotations: {}
162 | # kubernetes.io/ingress.class: nginx
163 | # kubernetes.io/tls-acme: "true"
164 |
165 | # -- Piper ingress hosts
166 | ## Hostnames must be provided if Ingress is enabled.
167 | hosts:
168 | - host: piper.example.local
169 | paths:
170 | - path: /
171 | pathType: ImplementationSpecific
172 | # -- Controller ingress tls
173 | tls: []
174 | # - secretName: chart-example-tls
175 | # hosts:
176 | # - chart-example.local
177 |
178 | # -- Additional environment variables for Piper. A list of name/value maps.
179 | env: []
180 |
181 | # -- Resource limits and requests for the pods.
182 | resources:
183 | requests:
184 | cpu: 200m
185 | memory: 512Mi
186 |
187 | # -- [Node selector]
188 | nodeSelector: {}
189 |
190 | # -- [Tolerations] for use with node taints
191 | tolerations: []
192 |
193 | # -- Assign custom [affinity] rules to the deployment
194 | affinity: {}
195 |
196 | # -- Deployment and pods extra labels
197 | extraLabels: {}
198 |
199 | autoscaling:
200 | # -- Wheter to enable auto-scaling of piper.
201 | enabled: false
202 | # -- Minimum reoplicas of Piper.
203 | minReplicas: 1
204 | # -- Maximum reoplicas of Piper.
205 | maxReplicas: 5
206 | # -- CPU utilization percentage threshold.
207 | targetCPUUtilizationPercentage: 85
208 | # -- Memory utilization percentage threshold.
209 | targetMemoryUtilizationPercentage: 85
210 |
211 | # -- Volumes of Piper Pod.
212 | volumes: []
213 |
214 | # -- Volumes to mount to Piper container.
215 | volumeMounts: []
216 |
217 | # -- Specify postStart and preStop lifecycle hooks for Piper container
218 | lifecycle: {}
--------------------------------------------------------------------------------
/pkg/workflow_handler/workflows.go:
--------------------------------------------------------------------------------
1 | package workflow_handler
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
8 | wfClientSet "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned"
9 | "k8s.io/apimachinery/pkg/apis/meta/v1"
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 | "k8s.io/apimachinery/pkg/types"
12 | "k8s.io/apimachinery/pkg/watch"
13 | "log"
14 | "strings"
15 |
16 | "github.com/rookout/piper/pkg/common"
17 | "github.com/rookout/piper/pkg/conf"
18 | "github.com/rookout/piper/pkg/utils"
19 | )
20 |
21 | const (
22 | ENTRYPOINT = "entryPoint"
23 | ONEXIT = "exitHandler"
24 | )
25 |
26 | type WorkflowsClientImpl struct {
27 | clientSet *wfClientSet.Clientset
28 | cfg *conf.GlobalConfig
29 | }
30 |
31 | func NewWorkflowsClient(cfg *conf.GlobalConfig) (WorkflowsClient, error) {
32 | restClientConfig, err := utils.GetClientConfig(cfg.WorkflowServerConfig.KubeConfig)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | clientSet := wfClientSet.NewForConfigOrDie(restClientConfig) //.ArgoprojV1alpha1().Workflows(namespace)
38 | return &WorkflowsClientImpl{
39 | clientSet: clientSet,
40 | cfg: cfg,
41 | }, nil
42 | }
43 |
44 | func (wfc *WorkflowsClientImpl) ConstructTemplates(workflowsBatch *common.WorkflowsBatch, configName string) ([]v1alpha1.Template, error) {
45 | finalTemplate := make([]v1alpha1.Template, 0)
46 | onStart, err := CreateDAGTemplate(workflowsBatch.OnStart, ENTRYPOINT)
47 | if err != nil {
48 | return nil, err
49 | }
50 | finalTemplate = append(finalTemplate, *onStart)
51 |
52 | onExit, err := CreateDAGTemplate(workflowsBatch.OnExit, ONEXIT)
53 | if err != nil {
54 | return nil, err
55 | }
56 | if onExit == nil || len(onExit.DAG.Tasks) == 0 {
57 | if IsConfigExists(&wfc.cfg.WorkflowsConfig, configName) && IsConfigsOnExitExists(&wfc.cfg.WorkflowsConfig, configName) {
58 | template := &v1alpha1.Template{
59 | Name: ONEXIT,
60 | DAG: &v1alpha1.DAGTemplate{
61 | Tasks: wfc.cfg.WorkflowsConfig.Configs[configName].OnExit,
62 | },
63 | }
64 |
65 | finalTemplate = append(finalTemplate, *template)
66 | }
67 | } else {
68 | finalTemplate = append(finalTemplate, *onExit)
69 | }
70 |
71 | finalTemplate, err = AddFilesToTemplates(finalTemplate, workflowsBatch.Templates)
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | return finalTemplate, nil
77 | }
78 |
79 | func (wfc *WorkflowsClientImpl) ConstructSpec(templates []v1alpha1.Template, params []v1alpha1.Parameter, configName string) (*v1alpha1.WorkflowSpec, error) {
80 | finalSpec := &v1alpha1.WorkflowSpec{}
81 | if IsConfigExists(&wfc.cfg.WorkflowsConfig, configName) {
82 | *finalSpec = wfc.cfg.WorkflowsConfig.Configs[configName].Spec
83 | if len(wfc.cfg.WorkflowsConfig.Configs[configName].OnExit) != 0 {
84 | finalSpec.OnExit = ONEXIT
85 | }
86 | }
87 |
88 | finalSpec.Entrypoint = ENTRYPOINT
89 | finalSpec.Templates = templates
90 | finalSpec.Arguments.Parameters = params
91 |
92 | return finalSpec, nil
93 | }
94 |
95 | func (wfc *WorkflowsClientImpl) CreateWorkflow(spec *v1alpha1.WorkflowSpec, workflowsBatch *common.WorkflowsBatch) (*v1alpha1.Workflow, error) {
96 | workflow := &v1alpha1.Workflow{
97 | ObjectMeta: metav1.ObjectMeta{
98 | GenerateName: ConvertToValidString(workflowsBatch.Payload.Repo + "-" + workflowsBatch.Payload.Branch + "-"),
99 | Namespace: wfc.cfg.Namespace,
100 | Labels: map[string]string{
101 | "piper.rookout.com/notified": "false",
102 | "repo": ConvertToValidString(workflowsBatch.Payload.Repo),
103 | "branch": ConvertToValidString(workflowsBatch.Payload.Branch),
104 | "user": ConvertToValidString(workflowsBatch.Payload.User),
105 | "commit": ConvertToValidString(workflowsBatch.Payload.Commit),
106 | },
107 | },
108 | Spec: *spec,
109 | }
110 |
111 | return workflow, nil
112 | }
113 |
114 | func (wfc *WorkflowsClientImpl) SelectConfig(workflowsBatch *common.WorkflowsBatch) (string, error) {
115 | var configName string
116 | if IsConfigExists(&wfc.cfg.WorkflowsConfig, "default") {
117 | configName = "default"
118 | }
119 |
120 | if *workflowsBatch.Config != "" {
121 | if IsConfigExists(&wfc.cfg.WorkflowsConfig, *workflowsBatch.Config) {
122 | configName = *workflowsBatch.Config
123 | } else {
124 | return configName, fmt.Errorf(
125 | "error in selecting config, staying with default config for repo %s branch %s",
126 | workflowsBatch.Payload.Repo,
127 | workflowsBatch.Payload.Branch,
128 | )
129 | }
130 | }
131 |
132 | log.Printf(
133 | "%s config selected for workflow in repo: %s branch %s",
134 | configName,
135 | workflowsBatch.Payload.Repo,
136 | workflowsBatch.Payload.Branch,
137 | ) // Info
138 |
139 | return configName, nil
140 | }
141 |
142 | func (wfc *WorkflowsClientImpl) Lint(wf *v1alpha1.Workflow) error {
143 | //TODO implement me
144 | panic("implement me")
145 | }
146 |
147 | func (wfc *WorkflowsClientImpl) Submit(ctx *context.Context, wf *v1alpha1.Workflow) error {
148 | workflowsClient := wfc.clientSet.ArgoprojV1alpha1().Workflows(wfc.cfg.Namespace)
149 | _, err := workflowsClient.Create(*ctx, wf, metav1.CreateOptions{})
150 | if err != nil {
151 | return err
152 | }
153 |
154 | return nil
155 | }
156 |
157 | func (wfc *WorkflowsClientImpl) HandleWorkflowBatch(ctx *context.Context, workflowsBatch *common.WorkflowsBatch) error {
158 | var params []v1alpha1.Parameter
159 |
160 | configName, err := wfc.SelectConfig(workflowsBatch)
161 | if err != nil {
162 | return err
163 | }
164 |
165 | templates, err := wfc.ConstructTemplates(workflowsBatch, configName)
166 | if err != nil {
167 | return err
168 | }
169 |
170 | if workflowsBatch.Parameters != nil {
171 | params, err = GetParameters(workflowsBatch.Parameters)
172 | if err != nil {
173 | return err
174 | }
175 | }
176 |
177 | globalParams := []v1alpha1.Parameter{
178 | {Name: "event", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.Event)},
179 | {Name: "action", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.Action)},
180 | {Name: "repo", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.Repo)},
181 | {Name: "branch", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.Branch)},
182 | {Name: "commit", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.Commit)},
183 | {Name: "user", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.User)},
184 | {Name: "user_email", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.UserEmail)},
185 | {Name: "pull_request_url", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.PullRequestURL)},
186 | {Name: "pull_request_title", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.PullRequestTitle)},
187 | {Name: "dest_branch", Value: v1alpha1.AnyStringPtr(workflowsBatch.Payload.DestBranch)},
188 | {Name: "pull_request_labels", Value: v1alpha1.AnyStringPtr(strings.Join(workflowsBatch.Payload.Labels, ","))},
189 | }
190 |
191 | params = append(params, globalParams...)
192 |
193 | spec, err := wfc.ConstructSpec(templates, params, configName)
194 | if err != nil {
195 | return err
196 | }
197 |
198 | workflow, err := wfc.CreateWorkflow(spec, workflowsBatch)
199 | if err != nil {
200 | return err
201 | }
202 |
203 | err = wfc.Submit(ctx, workflow)
204 | if err != nil {
205 | return fmt.Errorf("failed to submit workflow, error: %v", err)
206 | }
207 |
208 | log.Printf("submit workflow for branch %s repo %s commit %s", workflowsBatch.Payload.Branch, workflowsBatch.Payload.Repo, workflowsBatch.Payload.Commit)
209 | return nil
210 | }
211 |
212 | func (wfc *WorkflowsClientImpl) Watch(ctx *context.Context, labelSelector *metav1.LabelSelector) (watch.Interface, error) {
213 | workflowsClient := wfc.clientSet.ArgoprojV1alpha1().Workflows(wfc.cfg.Namespace)
214 | opts := v1.ListOptions{
215 | Watch: true,
216 | LabelSelector: metav1.FormatLabelSelector(labelSelector),
217 | }
218 | watcher, err := workflowsClient.Watch(*ctx, opts)
219 | if err != nil {
220 | return nil, err
221 | }
222 |
223 | return watcher, nil
224 | }
225 |
226 | func (wfc *WorkflowsClientImpl) UpdatePiperWorkflowLabel(ctx *context.Context, workflowName string, label string, value string) error {
227 | workflowsClient := wfc.clientSet.ArgoprojV1alpha1().Workflows(wfc.cfg.Namespace)
228 |
229 | patch, err := json.Marshal(map[string]interface{}{"metadata": metav1.ObjectMeta{
230 | Labels: map[string]string{
231 | fmt.Sprintf("piper.rookout.com/%s", label): value,
232 | },
233 | }})
234 | if err != nil {
235 | return err
236 | }
237 | _, err = workflowsClient.Patch(*ctx, workflowName, types.MergePatchType, patch, v1.PatchOptions{})
238 | if err != nil {
239 | return err
240 | }
241 |
242 | fmt.Printf("workflow %s labels piper.rookout.com/%s updated to %s\n", workflowName, label, value)
243 | return nil
244 | }
245 |
--------------------------------------------------------------------------------
/pkg/git_provider/bitbucket.go:
--------------------------------------------------------------------------------
1 | package git_provider
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/ktrysmt/go-bitbucket"
8 | "github.com/rookout/piper/pkg/conf"
9 | "github.com/rookout/piper/pkg/utils"
10 | "github.com/tidwall/gjson"
11 | "golang.org/x/net/context"
12 | "io"
13 | "log"
14 | "net/http"
15 | "strings"
16 | )
17 |
18 | type BitbucketClientImpl struct {
19 | client *bitbucket.Client
20 | cfg *conf.GlobalConfig
21 | HooksHashTable map[string]int64
22 | }
23 |
24 | func NewBitbucketServerClient(cfg *conf.GlobalConfig) (Client, error) {
25 | client := bitbucket.NewOAuthbearerToken(cfg.GitProviderConfig.Token)
26 |
27 | err := ValidateBitbucketPermissions(client, cfg)
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | return &BitbucketClientImpl{
33 | client: client,
34 | cfg: cfg,
35 | HooksHashTable: make(map[string]int64),
36 | }, err
37 | }
38 |
39 | func (b BitbucketClientImpl) ListFiles(ctx *context.Context, repo string, branch string, path string) ([]string, error) {
40 | var filesList []string
41 | fileOptions := bitbucket.RepositoryFilesOptions{
42 | Owner: b.cfg.GitProviderConfig.OrgName,
43 | RepoSlug: repo,
44 | Ref: branch,
45 | Path: path,
46 | MaxDepth: 0,
47 | }
48 | files, err := b.client.Repositories.Repository.ListFiles(&fileOptions)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | for _, f := range files {
54 | fileWithoutPath := strings.ReplaceAll(f.Path, path+"/", "")
55 | filesList = append(filesList, fileWithoutPath)
56 | }
57 |
58 | return filesList, nil
59 | }
60 |
61 | func (b BitbucketClientImpl) GetFile(ctx *context.Context, repo string, branch string, path string) (*CommitFile, error) {
62 | fileOptions := bitbucket.RepositoryFilesOptions{
63 | Owner: b.cfg.GitProviderConfig.OrgName,
64 | RepoSlug: repo,
65 | Ref: branch,
66 | Path: path,
67 | MaxDepth: 0,
68 | }
69 | fileContent, err := b.client.Repositories.Repository.GetFileContent(&fileOptions)
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | stringContent := string(fileContent[:])
75 | return &CommitFile{
76 | Path: &path,
77 | Content: &stringContent,
78 | }, nil
79 | }
80 |
81 | func (b BitbucketClientImpl) GetFiles(ctx *context.Context, repo string, branch string, paths []string) ([]*CommitFile, error) {
82 | var commitFiles []*CommitFile
83 | for _, path := range paths {
84 | file, err := b.GetFile(ctx, repo, branch, path)
85 | if err != nil {
86 | return nil, err
87 | }
88 | if file == nil {
89 | log.Printf("file %s not found in repo %s branch %s", path, repo, branch)
90 | continue
91 | }
92 | commitFiles = append(commitFiles, file)
93 | }
94 | return commitFiles, nil
95 | }
96 |
97 | func (b BitbucketClientImpl) SetWebhook(ctx *context.Context, repo *string) (*HookWithStatus, error) {
98 | webhookOptions := &bitbucket.WebhooksOptions{
99 | Owner: b.cfg.GitProviderConfig.OrgName,
100 | RepoSlug: *repo,
101 | Uuid: "",
102 | Description: "Piper",
103 | Url: b.cfg.GitProviderConfig.WebhookURL,
104 | Active: true,
105 | Events: []string{"repo:push", "pullrequest:created", "pullrequest:updated", "pullrequest:fulfilled"},
106 | }
107 |
108 | hook, exists := b.isRepoWebhookExists(*repo)
109 | if exists {
110 | log.Printf("webhook already exists for repository %s, skipping creation... \n", *repo)
111 | addHookToHashTable(utils.RemoveBraces(hook.Uuid), b.HooksHashTable)
112 | hookID, err := getHookByUUID(utils.RemoveBraces(hook.Uuid), b.HooksHashTable)
113 | if err != nil {
114 | return nil, err
115 | }
116 | return &HookWithStatus{
117 | HookID: hookID,
118 | HealthStatus: true,
119 | RepoName: repo,
120 | }, nil
121 | }
122 |
123 | hook, err := b.client.Repositories.Webhooks.Create(webhookOptions)
124 | if err != nil {
125 | return nil, err
126 | }
127 | log.Printf("created webhook for repository %s \n", *repo)
128 |
129 | addHookToHashTable(utils.RemoveBraces(hook.Uuid), b.HooksHashTable)
130 | hookID, err := getHookByUUID(utils.RemoveBraces(hook.Uuid), b.HooksHashTable)
131 | if err != nil {
132 | return nil, err
133 | }
134 |
135 | return &HookWithStatus{
136 | HookID: hookID,
137 | HealthStatus: true,
138 | RepoName: repo,
139 | }, nil
140 | }
141 |
142 | func (b BitbucketClientImpl) UnsetWebhook(ctx *context.Context, hook *HookWithStatus) error {
143 | //TODO implement me
144 | panic("implement me")
145 | }
146 |
147 | func (b BitbucketClientImpl) HandlePayload(ctx *context.Context, request *http.Request, secret []byte) (*WebhookPayload, error) {
148 | var webhookPayload *WebhookPayload
149 | var buf bytes.Buffer
150 |
151 | // Used for authentication, hookID generated by bitbucket.
152 | hookID, err := getHookByUUID(request.Header.Get("X-Hook-UUID"), b.HooksHashTable)
153 | if err != nil {
154 | return nil, fmt.Errorf("failed to get hook by UUID, %s", err)
155 | }
156 |
157 | _, err = io.Copy(&buf, request.Body)
158 | if err != nil {
159 | return nil, fmt.Errorf("error reading response: %s", err)
160 | }
161 |
162 | var body map[string]interface{}
163 | err = json.Unmarshal(buf.Bytes(), &body)
164 | if err != nil {
165 | return nil, fmt.Errorf("failed to unmarshal response: %s", err)
166 | }
167 |
168 | // https://support.atlassian.com/bitbucket-cloud/docs/event-payloads
169 | switch request.Header.Get("X-Event-Key") {
170 | case "repo:push":
171 | if gjson.GetBytes(buf.Bytes(), "push.changes.0.new.type").Value().(string) == "tag" {
172 | webhookPayload = &WebhookPayload{
173 | Event: "tag",
174 | Repo: utils.SanitizeString(gjson.GetBytes(buf.Bytes(), "repository.name").Value().(string)),
175 | Branch: gjson.GetBytes(buf.Bytes(), "push.changes.0.new.name").Value().(string),
176 | Commit: gjson.GetBytes(buf.Bytes(), "push.changes.0.new.name").Value().(string),
177 | User: gjson.GetBytes(buf.Bytes(), "actor.display_name").Value().(string),
178 | HookID: hookID,
179 | }
180 | } else {
181 | webhookPayload = &WebhookPayload{
182 | Event: "push",
183 | Repo: utils.SanitizeString(gjson.GetBytes(buf.Bytes(), "repository.name").Value().(string)),
184 | Branch: gjson.GetBytes(buf.Bytes(), "push.changes.0.new.name").Value().(string),
185 | Commit: gjson.GetBytes(buf.Bytes(), "push.changes.0.commits.0.hash").Value().(string),
186 | UserEmail: utils.ExtractStringsBetweenTags(gjson.GetBytes(buf.Bytes(), "push.changes.0.commits.0.author.raw").Value().(string))[0],
187 | User: gjson.GetBytes(buf.Bytes(), "actor.display_name").Value().(string),
188 | HookID: hookID,
189 | }
190 | }
191 |
192 | case "pullrequest:created", "pullrequest:updated":
193 | webhookPayload = &WebhookPayload{
194 | Event: "pull_request",
195 | Repo: utils.SanitizeString(gjson.GetBytes(buf.Bytes(), "repository.name").Value().(string)),
196 | Branch: gjson.GetBytes(buf.Bytes(), "pullrequest.source.branch.name").Value().(string),
197 | Commit: gjson.GetBytes(buf.Bytes(), "pullrequest.source.commit.hash").Value().(string),
198 | User: gjson.GetBytes(buf.Bytes(), "pullrequest.author.display_name").Value().(string),
199 | PullRequestURL: gjson.GetBytes(buf.Bytes(), "pullrequest.links.html.href").Value().(string),
200 | PullRequestTitle: gjson.GetBytes(buf.Bytes(), "pullrequest.title").Value().(string),
201 | DestBranch: gjson.GetBytes(buf.Bytes(), "pullrequest.destination.branch.name").Value().(string),
202 | HookID: hookID,
203 | }
204 | }
205 | return webhookPayload, nil
206 | }
207 |
208 | func (b BitbucketClientImpl) SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error {
209 | commitOptions := bitbucket.CommitsOptions{
210 | Owner: b.cfg.GitProviderConfig.OrgName,
211 | RepoSlug: *repo,
212 | Revision: *commit,
213 | }
214 | commitStatusOptions := bitbucket.CommitStatusOptions{
215 | Key: "build",
216 | Url: *linkURL,
217 | State: *status,
218 | Description: *message,
219 | }
220 | _, err := b.client.Repositories.Commits.CreateCommitStatus(&commitOptions, &commitStatusOptions)
221 | if err != nil {
222 | return err
223 | }
224 | log.Printf("set status of commit %s in repo %s to %s", *commit, *repo, *status)
225 | return nil
226 | }
227 |
228 | func (b BitbucketClientImpl) PingHook(ctx *context.Context, hook *HookWithStatus) error {
229 | //TODO implement me
230 | panic("implement me")
231 | }
232 |
233 | func (b BitbucketClientImpl) isRepoWebhookExists(repo string) (*bitbucket.Webhook, bool) {
234 | emptyHook := bitbucket.Webhook{}
235 |
236 | webhookOptions := bitbucket.WebhooksOptions{
237 | Owner: b.cfg.GitProviderConfig.OrgName,
238 | RepoSlug: repo,
239 | }
240 | hooks, err := b.client.Repositories.Webhooks.List(&webhookOptions)
241 |
242 | if err != nil {
243 | log.Printf("failed to list existing hooks for repository %s. error:%s", repo, err)
244 | return &emptyHook, false
245 | }
246 |
247 | if len(hooks) == 0 {
248 | return &emptyHook, false
249 | }
250 |
251 | for _, hook := range hooks {
252 | if hook.Description == "Piper" && hook.Url == b.cfg.GitProviderConfig.WebhookURL {
253 | return &hook, true
254 | }
255 | }
256 |
257 | return &emptyHook, false
258 | }
259 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2023 George Dozoretz
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------