├── .gitignore ├── hack ├── custom-boilerplate.go.txt ├── update-codegen.sh └── generate-groups.sh ├── charts └── captain-hook │ ├── ci │ └── pipedream-values.yaml │ ├── Chart.yaml │ ├── .helmignore │ ├── templates │ ├── service.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── hpa.yaml │ ├── serviceaccount.yaml │ ├── NOTES.txt │ ├── ingress.yaml │ ├── _helpers.tpl │ ├── crds.yaml │ └── deployment.yaml │ ├── README.md.gotmpl │ ├── values.yaml │ └── README.md ├── pkg ├── api │ └── captainhookio │ │ ├── v1alpha1 │ │ ├── doc.go │ │ ├── register.go │ │ ├── types_hook.go │ │ └── zz_generated.deepcopy.go │ │ └── register.go ├── client │ ├── clientset │ │ └── versioned │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ └── clientset_generated.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── typed │ │ │ └── captainhookio │ │ │ │ └── v1alpha1 │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_captainhookio_client.go │ │ │ │ └── fake_hook.go │ │ │ │ ├── doc.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── captainhookio_client.go │ │ │ │ └── hook.go │ │ │ └── clientset.go │ ├── listers │ │ └── captainhookio │ │ │ └── v1alpha1 │ │ │ ├── expansion_generated.go │ │ │ └── hook.go │ └── informers │ │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── captainhookio │ │ ├── v1alpha1 │ │ │ ├── interface.go │ │ │ └── hook.go │ │ └── interface.go │ │ ├── generic.go │ │ └── factory.go ├── version │ └── info.go ├── util │ ├── namespace_test.go │ └── namespace.go ├── hook │ ├── helpers.go │ ├── types.go │ ├── store_handler.go │ ├── fake.go │ ├── store_handler_test.go │ ├── sender_test.go │ ├── sender.go │ ├── handler_test.go │ ├── handler.go │ ├── informer.go │ └── testdata │ │ └── push.json ├── store │ ├── interface.go │ ├── fake.go │ ├── kubernetes_store.go │ └── kubernetes_store_test.go └── cmd │ └── listen.go ├── ct.yaml ├── .github ├── dependabot.yml ├── workflows │ ├── go.yml │ ├── golangci-lint.yml │ ├── docs.yaml │ ├── pr.yaml │ ├── main.yaml │ └── release.yaml └── stale.yml ├── Dockerfile ├── go.mod ├── README.md ├── .goreleaser.yml ├── cmd └── captain-hook │ └── captain-hook.go ├── .golangci.yml ├── Makefile └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage.out 3 | build 4 | dist 5 | -------------------------------------------------------------------------------- /hack/custom-boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | -------------------------------------------------------------------------------- /charts/captain-hook/ci/pipedream-values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | forwardURL: https://3ab9495928898d81fb9a62c113291fa6.m.pipedream.net 3 | -------------------------------------------------------------------------------- /pkg/api/captainhookio/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:openapi-gen=true 3 | // +groupName=captainhook.io 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /ct.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://github.com/helm/chart-testing#configuration 3 | remote: origin 4 | target-branch: main 5 | chart-dirs: 6 | - charts 7 | helm-extra-args: --timeout 120s 8 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | // This package has the automatically generated clientset. 8 | package versioned 9 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | // This package has the automatically generated fake clientset. 8 | package fake 9 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | // This package contains the scheme of the automatically generated clientset. 8 | package scheme 9 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | // Package fake has the automatically generated clients. 8 | package fake 9 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | // This package has the automatically generated typed clients. 8 | package v1alpha1 9 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | type HookExpansion interface{} 10 | -------------------------------------------------------------------------------- /pkg/version/info.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Build information. Populated at build-time. 4 | var ( 5 | Version string 6 | Revision string 7 | Branch string 8 | BuildDate string 9 | GoVersion string 10 | BuiltBy string 11 | ) 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "docker" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /pkg/util/namespace_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNamespace(t *testing.T) { 11 | os.Setenv("POD_NAMESPACE", "foobar") 12 | ns, err := Namespace() 13 | assert.NoError(t, err) 14 | assert.Equal(t, "foobar", ns) 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM} alpine:3.15.2 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG TARGETPLATFORM 6 | 7 | LABEL maintainer="Gareth Evans " 8 | COPY dist/captain-hook-${TARGETOS}_${TARGETOS}_${TARGETARCH}/captain-hook /usr/bin/captain-hook 9 | 10 | ENTRYPOINT [ "/usr/bin/captain-hook" ] 11 | 12 | CMD [ "listen" , "--debug" ] 13 | -------------------------------------------------------------------------------- /charts/captain-hook/Chart.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v2 3 | name: captain-hook 4 | description: A Helm chart for github.com/jenkins-infra/captain-hook 5 | home: https://github.com/jenkins-infra/captain-hook 6 | type: application 7 | version: 0.0.1-SNAPSHOT 8 | appVersion: dev 9 | maintainers: 10 | - name: garethjevans 11 | email: gareth@bryncynfelin.co.uk 12 | icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jenkins-infra/captain-hook 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/cenkalti/backoff v2.2.1+incompatible 7 | github.com/google/uuid v1.3.0 8 | github.com/gorilla/mux v1.8.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/sirupsen/logrus v1.8.1 11 | github.com/spf13/cobra v1.2.1 12 | github.com/spf13/pflag v1.0.5 13 | github.com/stretchr/testify v1.7.0 14 | k8s.io/apimachinery v0.22.4 15 | k8s.io/client-go v0.22.4 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/client/listers/captainhookio/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by lister-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | // HookListerExpansion allows custom methods to be added to 10 | // HookLister. 11 | type HookListerExpansion interface{} 12 | 13 | // HookNamespaceListerExpansion allows custom methods to be added to 14 | // HookNamespaceLister. 15 | type HookNamespaceListerExpansion interface{} 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build and test Go 2 | on: 3 | pull_request: 4 | jobs: 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Set up Go 1.16 10 | uses: actions/setup-go@v2 11 | with: 12 | go-version: 1.16 13 | - name: Check out source code 14 | uses: actions/checkout@v2 15 | - name: Build 16 | run: make build 17 | - name: Test 18 | run: make test 19 | -------------------------------------------------------------------------------- /charts/captain-hook/.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 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "captain-hook.fullname" . }} 6 | labels: 7 | {{- include "captain-hook.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | {{- include "captain-hook.selectorLabels" . | nindent 4 }} 17 | -------------------------------------------------------------------------------- /pkg/util/namespace.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func Namespace() (string, error) { 10 | if ns := os.Getenv("POD_NAMESPACE"); ns != "" { 11 | return ns, nil 12 | } 13 | if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { 14 | if ns := strings.TrimSpace(string(data)); len(ns) > 0 { 15 | return ns, nil 16 | } 17 | return "", err 18 | } 19 | return "", nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/api/captainhookio/register.go: -------------------------------------------------------------------------------- 1 | package captainhookio 2 | 3 | const ( 4 | // GroupName is the Captain Hook API group name 5 | GroupName = "captainhook.io" 6 | 7 | // Version is the Captain Hook API group version 8 | Version = "v1alpha1" 9 | 10 | // GroupAndVersion is the Captain Hook API Group and version 11 | GroupAndVersion = GroupName + "/" + Version 12 | 13 | // Package is the Go package in which the apis live 14 | Package = "github.com/jenkins-infra/captain-hook/pkg/api" 15 | ) 16 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | pull_request: 4 | jobs: 5 | golangci: 6 | name: lint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: golangci-lint 11 | uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 # v2 12 | with: 13 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 14 | version: v1.28 15 | -------------------------------------------------------------------------------- /pkg/hook/helpers.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func writeResult(w io.Writer, message string) { 12 | _, err := w.Write([]byte(message)) 13 | if err != nil { 14 | logrus.Debugf("failed to write message: %s, err: %s", message, err) 15 | } 16 | } 17 | 18 | func responseHTTPError(w http.ResponseWriter, statusCode int, message string, args ...interface{}) { 19 | response := fmt.Sprintf(message, args...) 20 | http.Error(w, response, statusCode) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/store/interface.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | // Store interface to implement a storage strategy. 4 | type Store interface { 5 | // StoreHook stores a webhook in the store. 6 | StoreHook(forwardURL string, body []byte, headers map[string][]string) (string, error) 7 | 8 | // Success marks a hook as successful. 9 | Success(id string) error 10 | 11 | // Marks a hook as error, with the error message. 12 | Error(id string, message string) error 13 | 14 | // Deletes a hook from the store. 15 | Delete(id string) error 16 | 17 | // Updates a hook to state it has been reattempted. 18 | MarkForRetry(id string) error 19 | } 20 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "captain-hook.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "captain-hook.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: curl 12 | image: curlimages/curl:7.75.0 13 | command: ['curl'] 14 | # this should be --fail-with-body when 7.76.0 is available 15 | args: ['--fail','-X','POST','--data','test-message','http://{{ include "captain-hook.fullname" . }}:{{ .Values.service.port }}{{ .Values.hookPath }}'] 16 | restartPolicy: Never 17 | -------------------------------------------------------------------------------- /pkg/hook/types.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import v1alpha12 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 4 | 5 | // Hook struct to hold everything related to a hook. 6 | type Hook struct { 7 | Name string 8 | Namespace string 9 | ForwardURL string 10 | Headers map[string][]string 11 | Body []byte 12 | // Status? State? 13 | 14 | } 15 | 16 | // FromV1Alpha1Hook converts from a v1alpha1.Hook to a Hook. 17 | func FromV1Alpha1Hook(h *v1alpha12.Hook) Hook { 18 | return Hook{ 19 | Name: h.Name, 20 | Namespace: h.Namespace, 21 | ForwardURL: h.Spec.ForwardURL, 22 | Headers: h.Spec.Headers, 23 | Body: []byte(h.Spec.Body), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Number of days of inactivity before an issue becomes stale 3 | daysUntilStale: 60 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | # Issues with these labels will never be considered stale 7 | exemptLabels: 8 | - pinned 9 | - security 10 | # Label to use when marking an issue as stale 11 | staleLabel: wontfix 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /charts/captain-hook/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | {{ template "chart.description" . }} 3 | 4 | ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) 5 | 6 | ## Additional Information 7 | 8 | This chart is best installed in the same namespace as Jenkins so that it can route webhooks directly 9 | to the Jenkins service. 10 | 11 | ## Installing the Chart 12 | 13 | To install the chart `captain-hook`: 14 | 15 | ```console 16 | $ helm repo add {{ template "chart.name" . }} https://jenkins-infra.github.io/{{ template "chart.name" . }} 17 | $ helm install {{ template "chart.name" . }} {{ template "chart.name" . }}/{{ template "chart.name" . }} 18 | ``` 19 | 20 | {{ template "chart.requirementsSection" . }} 21 | 22 | {{ template "chart.valuesSection" . }} 23 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/fake/fake_captainhookio_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package fake 8 | 9 | import ( 10 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/typed/captainhookio/v1alpha1" 11 | rest "k8s.io/client-go/rest" 12 | testing "k8s.io/client-go/testing" 13 | ) 14 | 15 | type FakeCaptainhookV1alpha1 struct { 16 | *testing.Fake 17 | } 18 | 19 | func (c *FakeCaptainhookV1alpha1) Hooks(namespace string) v1alpha1.HookInterface { 20 | return &FakeHooks{c, namespace} 21 | } 22 | 23 | // RESTClient returns a RESTClient that is used to communicate 24 | // with API server by this client implementation. 25 | func (c *FakeCaptainhookV1alpha1) RESTClient() rest.Interface { 26 | var ret *rest.RESTClient 27 | return ret 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # captain-hook 2 | 3 | a POC webhook relay that can be used to store and forward webhooks from GitHub to Jenkins. 4 | 5 | `storing` - is currently done in memory with a backoff strategy, but the intention is that this can be moved to something external like a DB/K8S/something else... 6 | 7 | ## Installation 8 | 9 | Should be installed in the same namespace as Jenkins. 10 | 11 | ``` 12 | helm repo add captain-hook https://jenkins-infra.github.io/captain-hook 13 | helm install captain-hook captain-hook/captain-hook 14 | ``` 15 | 16 | ## Configuration 17 | 18 | Configuration options on the helm chart can be found [here](charts/captain-hook/README.md). 19 | 20 | ## Debugging 21 | 22 | Once installed within a namespace, you can view the hooks within the system with: 23 | 24 | ``` 25 | kubectl get hooks 26 | ``` 27 | 28 | Or for more information: 29 | 30 | ``` 31 | kubectl get hooks -owide 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Update Docs 2 | on: 3 | schedule: 4 | - cron: "*/15 * * * *" 5 | jobs: 6 | build: 7 | name: Update Docs 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out source code 11 | uses: actions/checkout@v2 12 | - name: Update 13 | uses: docker://index.docker.io/jnorwood/helm-docs@sha256:66c8f4164dec860fa5c1528239c4aa826a12485305b7b224594b1a73f7e6879a # ratchet:docker://jnorwood/helm-docs:latest 14 | with: 15 | entrypoint: helm-docs 16 | - name: Debug 17 | run: | 18 | git diff 19 | git status 20 | - name: Create Pull Request 21 | id: cpr 22 | uses: peter-evans/create-pull-request@18f7dc018cc2cd597073088f7c7591b9d1c02672 # v3 23 | with: 24 | commit-message: 'chore(docs): regenerated helm docs' 25 | signoff: false 26 | title: 'chore(docs): regenerated helm docs' 27 | -------------------------------------------------------------------------------- /pkg/hook/store_handler.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "github.com/jenkins-infra/captain-hook/pkg/store" 5 | ) 6 | 7 | type handler struct { 8 | store store.Store 9 | sender Sender 10 | } 11 | 12 | func (h *handler) Handle(hook *Hook) error { 13 | // need to have a think about that the logic would be here. 14 | hookName, err := h.store.StoreHook(hook.ForwardURL, hook.Body, hook.Headers) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | hook.Name = hookName 20 | 21 | if h.sender == nil { 22 | h.sender = NewSender() 23 | } 24 | // attempt to send 25 | err = h.sender.send(hook.ForwardURL, hook.Body, hook.Headers) 26 | if err != nil { 27 | // if failed, mark as failed with the error as the message 28 | err = h.store.Error(hookName, err.Error()) 29 | if err != nil { 30 | return err 31 | } 32 | } else { 33 | // if success, mark as successful, 34 | err = h.store.Success(hookName) 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/hook/fake.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type fakeSender struct { 11 | fail bool 12 | Urls []string 13 | } 14 | 15 | func (f *fakeSender) send(forwardURL string, bodyBytes []byte, header map[string][]string) error { 16 | f.Urls = append(f.Urls, forwardURL) 17 | if f.fail { 18 | return errors.New("simulate a failure") 19 | } 20 | return nil 21 | } 22 | 23 | type FakeResponse struct { 24 | t *testing.T 25 | headers http.Header 26 | body []byte 27 | status int 28 | } 29 | 30 | func NewFakeRespone(t *testing.T) *FakeResponse { 31 | return &FakeResponse{ 32 | t: t, 33 | headers: make(http.Header), 34 | } 35 | } 36 | 37 | func (r *FakeResponse) Header() http.Header { 38 | return r.headers 39 | } 40 | 41 | func (r *FakeResponse) Write(body []byte) (int, error) { 42 | r.body = body 43 | return len(body), nil 44 | } 45 | 46 | func (r *FakeResponse) WriteHeader(status int) { 47 | r.status = status 48 | } 49 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "captain-hook.fullname" . }} 6 | labels: 7 | {{- include "captain-hook.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "captain-hook.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 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by informer-gen. DO NOT EDIT. 6 | 7 | package internalinterfaces 8 | 9 | import ( 10 | time "time" 11 | 12 | versioned "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | runtime "k8s.io/apimachinery/pkg/runtime" 15 | cache "k8s.io/client-go/tools/cache" 16 | ) 17 | 18 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 19 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 20 | 21 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 22 | type SharedInformerFactory interface { 23 | Start(stopCh <-chan struct{}) 24 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 25 | } 26 | 27 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 28 | type TweakListOptionsFunc func(*v1.ListOptions) 29 | -------------------------------------------------------------------------------- /pkg/store/fake.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "github.com/google/uuid" 4 | 5 | var _ Store = &FakeStore{} 6 | 7 | type FakeStore struct { 8 | MessageIDs []string 9 | SuccessMessageIDs []string 10 | ErroredMessageIDs []string 11 | DeletedMessageIDs []string 12 | RetryMessageIDs []string 13 | } 14 | 15 | func (s *FakeStore) StoreHook(forwardURL string, body []byte, header map[string][]string) (string, error) { 16 | messageID := uuid.New().String() 17 | s.MessageIDs = append(s.MessageIDs, messageID) 18 | return messageID, nil 19 | } 20 | 21 | func (s *FakeStore) Success(id string) error { 22 | s.SuccessMessageIDs = append(s.SuccessMessageIDs, id) 23 | return nil 24 | } 25 | 26 | func (s *FakeStore) Error(id string, message string) error { 27 | s.ErroredMessageIDs = append(s.ErroredMessageIDs, id) 28 | return nil 29 | } 30 | 31 | func (s *FakeStore) Delete(id string) error { 32 | s.DeletedMessageIDs = append(s.DeletedMessageIDs, id) 33 | return nil 34 | } 35 | 36 | func (s *FakeStore) MarkForRetry(id string) error { 37 | s.RetryMessageIDs = append(s.RetryMessageIDs, id) 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/hook/store_handler_test.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/jenkins-infra/captain-hook/pkg/store" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHandle_Success(t *testing.T) { 11 | s := store.FakeStore{} 12 | sender := fakeSender{} 13 | h := handler{ 14 | store: &s, 15 | sender: &sender, 16 | } 17 | 18 | hook := Hook{ 19 | ForwardURL: "http://example.com", 20 | } 21 | 22 | err := h.Handle(&hook) 23 | assert.NoError(t, err) 24 | 25 | assert.Equal(t, 1, len(s.MessageIDs)) 26 | assert.Equal(t, 1, len(s.SuccessMessageIDs)) 27 | assert.Equal(t, 0, len(s.ErroredMessageIDs)) 28 | } 29 | 30 | func TestHandle_Error(t *testing.T) { 31 | s := store.FakeStore{} 32 | sender := fakeSender{fail: true} 33 | h := handler{ 34 | store: &s, 35 | sender: &sender, 36 | } 37 | 38 | hook := Hook{ 39 | ForwardURL: "http://example.com", 40 | } 41 | 42 | err := h.Handle(&hook) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, 1, len(s.MessageIDs)) 46 | assert.Equal(t, 0, len(s.SuccessMessageIDs)) 47 | assert.Equal(t, 1, len(s.ErroredMessageIDs)) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/captainhookio/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by informer-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | internalinterfaces "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions/internalinterfaces" 11 | ) 12 | 13 | // Interface provides access to all the informers in this group version. 14 | type Interface interface { 15 | // Hooks returns a HookInformer. 16 | Hooks() HookInformer 17 | } 18 | 19 | type version struct { 20 | factory internalinterfaces.SharedInformerFactory 21 | namespace string 22 | tweakListOptions internalinterfaces.TweakListOptionsFunc 23 | } 24 | 25 | // New returns a new Interface. 26 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 27 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 28 | } 29 | 30 | // Hooks returns a HookInformer. 31 | func (v *version) Hooks() HookInformer { 32 | return &hookInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 33 | } 34 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/captainhookio/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by informer-gen. DO NOT EDIT. 6 | 7 | package captainhookio 8 | 9 | import ( 10 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions/captainhookio/v1alpha1" 11 | internalinterfaces "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions/internalinterfaces" 12 | ) 13 | 14 | // Interface provides access to each of this group's versions. 15 | type Interface interface { 16 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 17 | V1alpha1() v1alpha1.Interface 18 | } 19 | 20 | type group struct { 21 | factory internalinterfaces.SharedInformerFactory 22 | namespace string 23 | tweakListOptions internalinterfaces.TweakListOptionsFunc 24 | } 25 | 26 | // New returns a new Interface. 27 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 28 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 29 | } 30 | 31 | // V1alpha1 returns a new v1alpha1.Interface. 32 | func (g *group) V1alpha1() v1alpha1.Interface { 33 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 34 | } 35 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "captain-hook.serviceAccountName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "captain-hook.labels" . | nindent 4 }} 9 | {{- with .Values.serviceAccount.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: Role 16 | metadata: 17 | name: {{ include "captain-hook.serviceAccountName" . }} 18 | namespace: {{ .Release.Namespace }} 19 | rules: 20 | - apiGroups: 21 | - "captainhook.io" 22 | resources: 23 | - "hooks" 24 | verbs: 25 | - "get" 26 | - "watch" 27 | - "list" 28 | - "create" 29 | - "delete" 30 | - "update" 31 | --- 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: RoleBinding 34 | metadata: 35 | name: {{ include "captain-hook.serviceAccountName" . }} 36 | namespace: {{ .Release.Namespace }} 37 | subjects: 38 | - kind: ServiceAccount 39 | name: {{ include "captain-hook.serviceAccountName" . }} 40 | namespace: {{ .Release.Namespace }} 41 | roleRef: 42 | kind: Role 43 | name: {{ include "captain-hook.serviceAccountName" . }} 44 | apiGroup: rbac.authorization.k8s.io 45 | --- 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package fake 8 | 9 | import ( 10 | captainhookv1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | runtime "k8s.io/apimachinery/pkg/runtime" 13 | schema "k8s.io/apimachinery/pkg/runtime/schema" 14 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 15 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 16 | ) 17 | 18 | var scheme = runtime.NewScheme() 19 | var codecs = serializer.NewCodecFactory(scheme) 20 | 21 | var localSchemeBuilder = runtime.SchemeBuilder{ 22 | captainhookv1alpha1.AddToScheme, 23 | } 24 | 25 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 26 | // of clientsets, like in: 27 | // 28 | // import ( 29 | // "k8s.io/client-go/kubernetes" 30 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 31 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 32 | // ) 33 | // 34 | // kclientset, _ := kubernetes.NewForConfig(c) 35 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 36 | // 37 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 38 | // correctly. 39 | var AddToScheme = localSchemeBuilder.AddToScheme 40 | 41 | func init() { 42 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 43 | utilruntime.Must(AddToScheme(scheme)) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package scheme 8 | 9 | import ( 10 | captainhookv1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | runtime "k8s.io/apimachinery/pkg/runtime" 13 | schema "k8s.io/apimachinery/pkg/runtime/schema" 14 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 15 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 16 | ) 17 | 18 | var Scheme = runtime.NewScheme() 19 | var Codecs = serializer.NewCodecFactory(Scheme) 20 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 21 | var localSchemeBuilder = runtime.SchemeBuilder{ 22 | captainhookv1alpha1.AddToScheme, 23 | } 24 | 25 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 26 | // of clientsets, like in: 27 | // 28 | // import ( 29 | // "k8s.io/client-go/kubernetes" 30 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 31 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 32 | // ) 33 | // 34 | // kclientset, _ := kubernetes.NewForConfig(c) 35 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 36 | // 37 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 38 | // correctly. 39 | var AddToScheme = localSchemeBuilder.AddToScheme 40 | 41 | func init() { 42 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 43 | utilruntime.Must(AddToScheme(Scheme)) 44 | } 45 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | - GO111MODULE=on 4 | - CGO_ENABLED=0 5 | before: 6 | hooks: 7 | - go mod download 8 | 9 | builds: 10 | - id: captain-hook-linux 11 | main: ./cmd/captain-hook/captain-hook.go 12 | binary: captain-hook 13 | ldflags: 14 | - -X github.com/jenkins-infra/captain-hook/pkg/version.Version={{.Version}} -X github.com/jenkins-infra/captain-hook/pkg/version.Revision={{.ShortCommit}} -X github.com/jenkins-infra/captain-hook/pkg/version.BuildDate={{.CommitDate}} -X github.com/jenkins-infra/captain-hook/pkg/version.BuiltBy=goreleaser 15 | goos: 16 | - linux 17 | goarch: 18 | - amd64 19 | - arm64 20 | - s390x 21 | - ppc64le 22 | 23 | - id: captain-hook-darwin 24 | main: ./cmd/captain-hook/captain-hook.go 25 | binary: captain-hook 26 | ldflags: 27 | - -X github.com/jenkins-infra/captain-hook/pkg/version.Version={{.Version}} -X github.com/jenkins-infra/captain-hook/pkg/version.Revision={{.ShortCommit}} -X github.com/jenkins-infra/captain-hook/pkg/version.BuildDate={{.CommitDate}} -X github.com/jenkins-infra/captain-hook/pkg/version.BuiltBy=goreleaser 28 | goos: 29 | - darwin 30 | goarch: 31 | - amd64 32 | - arm64 33 | 34 | archives: 35 | - name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" 36 | format_overrides: 37 | - goos: windows 38 | format: zip 39 | 40 | checksum: 41 | name_template: "{{ .ProjectName }}-checksums.txt" 42 | algorithm: sha256 43 | 44 | changelog: 45 | skip: false 46 | 47 | release: 48 | draft: false 49 | prerelease: false 50 | name_template: "{{.Tag}}" 51 | -------------------------------------------------------------------------------- /pkg/api/captainhookio/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | // SchemeGroupVersion is group version used to register these objects. 11 | var SchemeGroupVersion = schema.GroupVersion{Group: captainhookio.GroupName, Version: captainhookio.Version} 12 | 13 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind. 14 | func Kind(kind string) schema.GroupKind { 15 | return SchemeGroupVersion.WithKind(kind).GroupKind() 16 | } 17 | 18 | // Resource takes an unqualified resource and returns a Group qualified GroupResource. 19 | func Resource(resource string) schema.GroupResource { 20 | return SchemeGroupVersion.WithResource(resource).GroupResource() 21 | } 22 | 23 | var ( 24 | // SchemeBuilder for building the schema :). 25 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 26 | // AddToScheme helper 27 | AddToScheme = SchemeBuilder.AddToScheme 28 | ) 29 | 30 | func init() { 31 | // We only register manually written functions here. The registration of the 32 | // generated functions takes place in the generated files. The separation 33 | // makes the code compile even when the generated files are missing. 34 | SchemeBuilder.Register(addKnownTypes) 35 | } 36 | 37 | // Adds the list of known types to Scheme. 38 | func addKnownTypes(scheme *runtime.Scheme) error { 39 | scheme.AddKnownTypes(SchemeGroupVersion, 40 | &Hook{}, 41 | &HookList{}, 42 | ) 43 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by informer-gen. DO NOT EDIT. 6 | 7 | package externalversions 8 | 9 | import ( 10 | "fmt" 11 | 12 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 13 | schema "k8s.io/apimachinery/pkg/runtime/schema" 14 | cache "k8s.io/client-go/tools/cache" 15 | ) 16 | 17 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 18 | // sharedInformers based on type 19 | type GenericInformer interface { 20 | Informer() cache.SharedIndexInformer 21 | Lister() cache.GenericLister 22 | } 23 | 24 | type genericInformer struct { 25 | informer cache.SharedIndexInformer 26 | resource schema.GroupResource 27 | } 28 | 29 | // Informer returns the SharedIndexInformer. 30 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 31 | return f.informer 32 | } 33 | 34 | // Lister returns the GenericLister. 35 | func (f *genericInformer) Lister() cache.GenericLister { 36 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 37 | } 38 | 39 | // ForResource gives generic access to a shared informer of the matching type 40 | // TODO extend this to unknown resources with a client pool 41 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 42 | switch resource { 43 | // Group=captainhook.io, Version=v1alpha1 44 | case v1alpha1.SchemeGroupVersion.WithResource("hooks"): 45 | return &genericInformer{resource: resource.GroupResource(), informer: f.Captainhook().V1alpha1().Hooks().Informer()}, nil 46 | 47 | } 48 | 49 | return nil, fmt.Errorf("no informer found for %v", resource) 50 | } 51 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "captain-hook.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "captain-hook.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "captain-hook.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "captain-hook.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "captain-hook.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- $kubeTargetVersion := default .Capabilities.KubeVersion.GitVersion .Values.kubeTargetVersionOverride }} 5 | {{- if semverCompare ">=1.19-0" $kubeTargetVersion -}} 6 | apiVersion: networking.k8s.io/v1 7 | {{- else if semverCompare ">=1.14-0" $kubeTargetVersion -}} 8 | apiVersion: networking.k8s.io/v1beta1 9 | {{- else -}} 10 | apiVersion: extensions/v1beta1 11 | {{- end }} 12 | kind: Ingress 13 | metadata: 14 | name: {{ $fullName }} 15 | labels: 16 | {{- include "captain-hook.labels" . | nindent 4 }} 17 | {{- with .Values.ingress.annotations }} 18 | annotations: 19 | {{- toYaml . | nindent 4 }} 20 | {{- end }} 21 | spec: 22 | {{- if .Values.ingress.ingressClassName }} 23 | ingressClassName: {{ .Values.ingress.ingressClassName }} 24 | {{- end }} 25 | {{- if .Values.ingress.tls }} 26 | tls: 27 | {{- range .Values.ingress.tls }} 28 | - hosts: 29 | {{- range .hosts }} 30 | - {{ . | quote }} 31 | {{- end }} 32 | secretName: {{ .secretName }} 33 | {{- end }} 34 | {{- end }} 35 | rules: 36 | {{- range .Values.ingress.hosts }} 37 | - host: {{ .host | quote }} 38 | http: 39 | paths: 40 | {{- range .paths }} 41 | - path: {{ .path }} 42 | {{- if semverCompare ">=1.19-0" $kubeTargetVersion }} 43 | backend: 44 | service: 45 | name: {{ $fullName }} 46 | port: 47 | number: {{ $svcPort }} 48 | pathType: ImplementationSpecific 49 | {{- else }} 50 | backend: 51 | serviceName: {{ $fullName }} 52 | servicePort: {{ $svcPort }} 53 | {{- end }} 54 | {{- end }} 55 | {{- end }} 56 | {{- end }} 57 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | #CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ./cmd/code-generator)} 22 | GENERATOR_VERSION=v0.20.4 23 | ( 24 | # To support running this script from anywhere, we have to first cd into this directory 25 | # so we can install the tools. 26 | cd "$(dirname "${0}")" 27 | go get k8s.io/code-generator/cmd/{defaulter-gen,client-gen,lister-gen,informer-gen,deepcopy-gen}@$GENERATOR_VERSION 28 | ) 29 | 30 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 31 | rm -rf "${SCRIPT_ROOT}"/pkg/client 32 | # generate the code with: 33 | # --output-base because this script should also be able to run inside the vendor dir of 34 | # k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir 35 | # instead of the $GOPATH directly. For normal projects this can be dropped. 36 | bash hack/generate-groups.sh all \ 37 | github.com/jenkins-infra/captain-hook/pkg/client github.com/jenkins-infra/captain-hook/pkg/api \ 38 | captainhookio:v1alpha1 \ 39 | --output-base "$(dirname "${BASH_SOURCE[0]}")/../../../.." \ 40 | --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt 41 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "captain-hook.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "captain-hook.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "captain-hook.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "captain-hook.labels" -}} 37 | helm.sh/chart: {{ include "captain-hook.chart" . }} 38 | {{ include "captain-hook.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "captain-hook.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "captain-hook.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "captain-hook.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "captain-hook.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | pull_request: 4 | jobs: 5 | goreleaser: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.16 16 | - name: Set up Helm 17 | uses: azure/setup-helm@18bc76811624f360dbd7f18c2d4ecb32c7b87bab # v1 18 | with: 19 | version: v3.5.2 20 | - uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.7 23 | - name: Prepare 24 | id: prep 25 | run: | 26 | DOCKER_IMAGE=jenkinsciinfra/captain-hook 27 | VERSION=latest 28 | GORELEASER_ARGS="build --rm-dist --snapshot" 29 | RELEASE_CHART=false 30 | 31 | if [[ $GITHUB_REF == refs/tags/* ]]; then 32 | # release 33 | VERSION=${GITHUB_REF#refs/tags/} 34 | GORELEASER_ARGS="release --rm-dist" 35 | RELEASE_CHART=true 36 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 37 | # branch 38 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 39 | if [[ $VERSION == 'main' ]]; then 40 | VERSION=latest 41 | fi 42 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 43 | # pr 44 | VERSION=pr-${{ github.event.number }} 45 | fi 46 | 47 | TAGS="${DOCKER_IMAGE}:${VERSION}" 48 | 49 | echo ::set-output name=release_chart::${RELEASE_CHART} 50 | echo ::set-output name=goreleaser_args::${GORELEASER_ARGS} 51 | echo ::set-output name=image::${DOCKER_IMAGE} 52 | echo ::set-output name=version::${VERSION} 53 | echo ::set-output name=tags::${TAGS} 54 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 55 | - name: Run GoReleaser 56 | uses: goreleaser/goreleaser-action@56f5b77f7fa4a8fe068bf22b732ec036cc9bc13f # v2.4.1 57 | with: 58 | version: latest 59 | args: ${{ steps.prep.outputs.goreleaser_args }} 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /pkg/hook/sender_test.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/sirupsen/logrus" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSender(t *testing.T) { 15 | logrus.SetLevel(logrus.DebugLevel) 16 | logrus.SetFormatter(&logrus.JSONFormatter{}) 17 | 18 | var tests = []struct { 19 | name string 20 | data string 21 | responseBody string 22 | responseStatus int 23 | error bool 24 | }{ 25 | { 26 | name: "ok", 27 | data: "testdata/push.json", 28 | responseBody: "OK", 29 | responseStatus: 200, 30 | }, 31 | { 32 | name: "no content", 33 | data: "testdata/push.json", 34 | responseBody: "OK", 35 | responseStatus: 204, 36 | }, 37 | { 38 | name: "bad request", 39 | data: "testdata/push.json", 40 | responseBody: "Not OK", 41 | responseStatus: 400, 42 | error: true, 43 | }, 44 | { 45 | name: "not found", 46 | data: "testdata/push.json", 47 | responseBody: "Not OK", 48 | responseStatus: 404, 49 | error: true, 50 | }, 51 | { 52 | name: "internal server error", 53 | data: "testdata/push.json", 54 | responseBody: "Not OK", 55 | responseStatus: 500, 56 | error: true, 57 | }, 58 | } 59 | for _, test := range tests { 60 | t.Run(test.name, func(t *testing.T) { 61 | handlerFunc := func(rw http.ResponseWriter, req *http.Request) { 62 | assert.Equal(t, req.URL.String(), "/") 63 | 64 | rw.WriteHeader(test.responseStatus) 65 | 66 | if test.responseStatus != 204 { 67 | writeResult(rw, test.responseBody) 68 | } 69 | } 70 | 71 | server := httptest.NewServer(http.HandlerFunc(handlerFunc)) 72 | // Close the server when test finishes 73 | defer server.Close() 74 | 75 | sender := sender{ 76 | client: server.Client(), 77 | } 78 | 79 | data, err := ioutil.ReadFile(test.data) 80 | assert.NoError(t, err) 81 | 82 | buf := bytes.NewBuffer(data) 83 | 84 | header := make(http.Header) 85 | err = sender.send(server.URL, buf.Bytes(), header) 86 | 87 | if test.error { 88 | assert.Error(t, err) 89 | } else { 90 | assert.NoError(t, err) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/captainhookio_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 11 | "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/scheme" 12 | rest "k8s.io/client-go/rest" 13 | ) 14 | 15 | type CaptainhookV1alpha1Interface interface { 16 | RESTClient() rest.Interface 17 | HooksGetter 18 | } 19 | 20 | // CaptainhookV1alpha1Client is used to interact with features provided by the captainhook.io group. 21 | type CaptainhookV1alpha1Client struct { 22 | restClient rest.Interface 23 | } 24 | 25 | func (c *CaptainhookV1alpha1Client) Hooks(namespace string) HookInterface { 26 | return newHooks(c, namespace) 27 | } 28 | 29 | // NewForConfig creates a new CaptainhookV1alpha1Client for the given config. 30 | func NewForConfig(c *rest.Config) (*CaptainhookV1alpha1Client, error) { 31 | config := *c 32 | if err := setConfigDefaults(&config); err != nil { 33 | return nil, err 34 | } 35 | client, err := rest.RESTClientFor(&config) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &CaptainhookV1alpha1Client{client}, nil 40 | } 41 | 42 | // NewForConfigOrDie creates a new CaptainhookV1alpha1Client for the given config and 43 | // panics if there is an error in the config. 44 | func NewForConfigOrDie(c *rest.Config) *CaptainhookV1alpha1Client { 45 | client, err := NewForConfig(c) 46 | if err != nil { 47 | panic(err) 48 | } 49 | return client 50 | } 51 | 52 | // New creates a new CaptainhookV1alpha1Client for the given RESTClient. 53 | func New(c rest.Interface) *CaptainhookV1alpha1Client { 54 | return &CaptainhookV1alpha1Client{c} 55 | } 56 | 57 | func setConfigDefaults(config *rest.Config) error { 58 | gv := v1alpha1.SchemeGroupVersion 59 | config.GroupVersion = &gv 60 | config.APIPath = "/apis" 61 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 62 | 63 | if config.UserAgent == "" { 64 | config.UserAgent = rest.DefaultKubernetesUserAgent() 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // RESTClient returns a RESTClient that is used to communicate 71 | // with API server by this client implementation. 72 | func (c *CaptainhookV1alpha1Client) RESTClient() rest.Interface { 73 | if c == nil { 74 | return nil 75 | } 76 | return c.restClient 77 | } 78 | -------------------------------------------------------------------------------- /pkg/hook/sender.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/cenkalti/backoff" 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var _ Sender = &sender{} 17 | 18 | type Sender interface { 19 | send(forwardURL string, bodyBytes []byte, header map[string][]string) error 20 | } 21 | 22 | type sender struct { 23 | client *http.Client 24 | InsecureRelay bool 25 | } 26 | 27 | func NewSender() Sender { 28 | return &sender{ 29 | InsecureRelay: os.Getenv("INSECURE_RELAY") == "true", 30 | } 31 | } 32 | 33 | func (s *sender) send(forwardURL string, bodyBytes []byte, header map[string][]string) error { 34 | logrus.Debugf("relaying %s", string(bodyBytes)) 35 | 36 | var httpClient *http.Client 37 | 38 | if s.client != nil { 39 | httpClient = s.client 40 | } else { 41 | if s.InsecureRelay { 42 | // #nosec G402 43 | tr := &http.Transport{ 44 | TLSClientConfig: &tls.Config{ 45 | InsecureSkipVerify: true, 46 | }, 47 | } 48 | 49 | httpClient = &http.Client{Transport: tr} 50 | } else { 51 | httpClient = &http.Client{} 52 | } 53 | } 54 | 55 | req, err := http.NewRequest("POST", forwardURL, bytes.NewReader(bodyBytes)) 56 | if err != nil { 57 | return err 58 | } 59 | req.Header = header 60 | 61 | resp, err := httpClient.Do(req) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | logrus.Infof("got resp code %d from url '%s'", resp.StatusCode, forwardURL) 67 | 68 | // If we got a 500, check if it's got the "repository not configured" string in the body. If so, we retry. 69 | if resp.StatusCode == 500 { 70 | respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 10000000)) 71 | if err != nil { 72 | return backoff.Permanent(errors.Wrap(err, "parsing resp.body")) 73 | } 74 | err = resp.Body.Close() 75 | if err != nil { 76 | return backoff.Permanent(errors.Wrap(err, "closing resp.body")) 77 | } 78 | logrus.Infof("got error respBody '%s'", string(respBody)) 79 | } 80 | 81 | // If we got anything other than a 2xx, retry as well. 82 | // We're leaving this distinct from the "not configured" behavior in case we want to resurrect that later. (apb) 83 | if resp.StatusCode < 200 || resp.StatusCode >= 400 { 84 | return errors.Errorf("%s not available, error was %s", req.URL.String(), resp.Status) 85 | } 86 | 87 | // And finally, if we haven't gotten any errors, just return nil because we're good. 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /charts/captain-hook/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for captain-hook. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # -- Number of replicas to run 6 | replicaCount: 1 7 | 8 | image: 9 | repository: jenkinsciinfra/captain-hook 10 | pullPolicy: IfNotPresent 11 | # -- Overrides the image tag whose default is the chart appVersion. 12 | tag: "" 13 | 14 | # -- Path to listen for webhook events on 15 | hookPath: /hook 16 | # -- Url to send all webhook events to 17 | forwardURL: http://jenkins:8080/github-webhook/ 18 | # -- Should we relay to insecure tls endpoints 19 | insecureRelay: false 20 | # -- Maximum age in seconds a successful webhook should be live for 21 | maxAgeInSeconds: 3600 22 | # -- Number of seconds the next retry should not be attempted before 23 | attemptRetryAfterInSeconds: 60 24 | # -- Maximum number of times this webhook should be attempted 25 | maxAttempts: 10 26 | 27 | imagePullSecrets: [] 28 | nameOverride: "" 29 | fullnameOverride: "" 30 | 31 | serviceAccount: 32 | # -- Specifies whether a service account should be created 33 | create: true 34 | # -- The name of the service account to use. If not set and create is true, a name is generated using the fullname template 35 | name: "" 36 | 37 | service: 38 | type: ClusterIP 39 | port: 8080 40 | 41 | ingress: 42 | # -- Create an ingress resource for this service 43 | enabled: true 44 | annotations: {} 45 | # kubernetes.io/tls-acme: "true" 46 | ingressClassName: "" 47 | hosts: 48 | - paths: 49 | - backend: 50 | service: 51 | name: captain-hook 52 | port: 53 | number: 8080 54 | 55 | tls: [] 56 | # - secretName: chart-example-tls 57 | # hosts: 58 | # - chart-example.local 59 | 60 | resources: {} 61 | # We usually recommend not to specify default resources and to leave this as a conscious 62 | # choice for the user. This also increases chances charts run on environments with little 63 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 64 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 65 | # limits: 66 | # cpu: 100m 67 | # memory: 128Mi 68 | # requests: 69 | # cpu: 100m 70 | # memory: 128Mi 71 | 72 | # -- Autoscaling configuration, disabled by default 73 | autoscaling: 74 | enabled: false 75 | minReplicas: 1 76 | maxReplicas: 100 77 | targetCPUUtilizationPercentage: 80 78 | # targetMemoryUtilizationPercentage: 80 79 | -------------------------------------------------------------------------------- /pkg/hook/handler_test.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/jenkins-infra/captain-hook/pkg/store" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestWebhooks(t *testing.T) { 18 | logrus.SetLevel(logrus.DebugLevel) 19 | logrus.SetFormatter(&logrus.JSONFormatter{}) 20 | 21 | var tests = []struct { 22 | name string 23 | event string 24 | before string 25 | handlerFunc func(rw http.ResponseWriter, req *http.Request) 26 | }{ 27 | // push 28 | { 29 | name: "should_relay", 30 | event: "push", 31 | before: "testdata/push.json", 32 | handlerFunc: func(rw http.ResponseWriter, req *http.Request) { 33 | // Test request parameters 34 | assert.Equal(t, req.URL.String(), "/") 35 | 36 | // Send response to be tested 37 | t.Logf("sending 'OK'") 38 | _, err := rw.Write([]byte(`OK`)) 39 | assert.NoError(t, err) 40 | }, 41 | }, 42 | // any other error 43 | { 44 | name: "error", 45 | event: "push", 46 | before: "testdata/push.json", 47 | handlerFunc: func(rw http.ResponseWriter, req *http.Request) { 48 | // Test request parameters 49 | assert.Equal(t, req.URL.String(), "/") 50 | 51 | // Send response to be tested 52 | rw.WriteHeader(500) 53 | t.Logf("sending 'not ok'") 54 | _, err := rw.Write([]byte(`not ok`)) 55 | assert.NoError(t, err) 56 | }, 57 | }, 58 | } 59 | for _, test := range tests { 60 | t.Run(test.name, func(t *testing.T) { 61 | before, err := ioutil.ReadFile(test.before) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | 66 | buf := bytes.NewBuffer(before) 67 | r, _ := http.NewRequest("POST", "/", buf) 68 | r.Header.Set("X-GitHub-Event", test.event) 69 | r.Header.Set("X-GitHub-Delivery", "f2467dea-70d6-11e8-8955-3c83993e0aef") 70 | 71 | hf := func(rw http.ResponseWriter, req *http.Request) { 72 | test.handlerFunc(rw, req) 73 | } 74 | 75 | server := httptest.NewServer(http.HandlerFunc(hf)) 76 | // Close the server when test finishes 77 | defer server.Close() 78 | 79 | o := Options{ 80 | ForwardURL: server.URL, 81 | handler: &handler{ 82 | store: &store.FakeStore{}, 83 | sender: &sender{ 84 | client: server.Client(), 85 | InsecureRelay: false, 86 | }, 87 | }, 88 | } 89 | 90 | w := NewFakeRespone(t) 91 | o.handleWebHookRequests(w, r) 92 | 93 | assert.Equal(t, string(w.body), "OK") 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /charts/captain-hook/README.md: -------------------------------------------------------------------------------- 1 | # captain-hook 2 | 3 | A Helm chart for github.com/jenkins-infra/captain-hook 4 | 5 | ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) 6 | 7 | ## Additional Information 8 | 9 | This chart is best installed in the same namespace as Jenkins so that it can route webhooks directly 10 | to the Jenkins service. 11 | 12 | ## Installing the Chart 13 | 14 | To install the chart `captain-hook`: 15 | 16 | ```console 17 | $ helm repo add captain-hook https://jenkins-infra.github.io/captain-hook 18 | $ helm install captain-hook captain-hook/captain-hook 19 | ``` 20 | 21 | ## Values 22 | 23 | | Key | Type | Default | Description | 24 | |-----|------|---------|-------------| 25 | | attemptRetryAfterInSeconds | int | `60` | Number of seconds the next retry should not be attempted before | 26 | | autoscaling | object | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Autoscaling configuration, disabled by default | 27 | | forwardURL | string | `"http://jenkins:8080/github-webhook/"` | Url to send all webhook events to | 28 | | fullnameOverride | string | `""` | | 29 | | hookPath | string | `"/hook"` | Path to listen for webhook events on | 30 | | image.pullPolicy | string | `"IfNotPresent"` | | 31 | | image.repository | string | `"jenkinsciinfra/captain-hook"` | | 32 | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | 33 | | imagePullSecrets | list | `[]` | | 34 | | ingress.annotations | object | `{}` | | 35 | | ingress.enabled | bool | `true` | Create an ingress resource for this service | 36 | | ingress.hosts[0].paths[0].backend.service.name | string | `"captain-hook"` | | 37 | | ingress.hosts[0].paths[0].backend.service.port.number | int | `8080` | | 38 | | ingress.ingressClassName | string | `""` | | 39 | | ingress.tls | list | `[]` | | 40 | | insecureRelay | bool | `false` | Should we relay to insecure tls endpoints | 41 | | maxAgeInSeconds | int | `3600` | Maximum age in seconds a successful webhook should be live for | 42 | | maxAttempts | int | `10` | Maximum number of times this webhook should be attempted | 43 | | nameOverride | string | `""` | | 44 | | replicaCount | int | `1` | Number of replicas to run | 45 | | resources | object | `{}` | | 46 | | service.port | int | `8080` | | 47 | | service.type | string | `"ClusterIP"` | | 48 | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created | 49 | | 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 | 50 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package fake 8 | 9 | import ( 10 | clientset "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned" 11 | captainhookv1alpha1 "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/typed/captainhookio/v1alpha1" 12 | fakecaptainhookv1alpha1 "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/fake" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/watch" 15 | "k8s.io/client-go/discovery" 16 | fakediscovery "k8s.io/client-go/discovery/fake" 17 | "k8s.io/client-go/testing" 18 | ) 19 | 20 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 21 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 22 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 23 | // for a real clientset and is mostly useful in simple unit tests. 24 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 25 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 26 | for _, obj := range objects { 27 | if err := o.Add(obj); err != nil { 28 | panic(err) 29 | } 30 | } 31 | 32 | cs := &Clientset{tracker: o} 33 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 34 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 35 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 36 | gvr := action.GetResource() 37 | ns := action.GetNamespace() 38 | watch, err := o.Watch(gvr, ns) 39 | if err != nil { 40 | return false, nil, err 41 | } 42 | return true, watch, nil 43 | }) 44 | 45 | return cs 46 | } 47 | 48 | // Clientset implements clientset.Interface. Meant to be embedded into a 49 | // struct to get a default implementation. This makes faking out just the method 50 | // you want to test easier. 51 | type Clientset struct { 52 | testing.Fake 53 | discovery *fakediscovery.FakeDiscovery 54 | tracker testing.ObjectTracker 55 | } 56 | 57 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 58 | return c.discovery 59 | } 60 | 61 | func (c *Clientset) Tracker() testing.ObjectTracker { 62 | return c.tracker 63 | } 64 | 65 | var _ clientset.Interface = &Clientset{} 66 | 67 | // CaptainhookV1alpha1 retrieves the CaptainhookV1alpha1Client 68 | func (c *Clientset) CaptainhookV1alpha1() captainhookv1alpha1.CaptainhookV1alpha1Interface { 69 | return &fakecaptainhookv1alpha1.FakeCaptainhookV1alpha1{Fake: &c.Fake} 70 | } 71 | -------------------------------------------------------------------------------- /pkg/api/captainhookio/v1alpha1/types_hook.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:noStatus 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | // +k8s:openapi-gen=true 11 | 12 | // Hook represents a webhook. 13 | type Hook struct { 14 | metav1.TypeMeta `json:",inline"` 15 | // Standard object's metadata. 16 | // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata 17 | // +optional 18 | metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 19 | 20 | Spec HookSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 21 | Status HookStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` 22 | } 23 | 24 | // HookSpec is the specification of a Hook. 25 | type HookSpec struct { 26 | ForwardURL string `json:"forwardURL" protobuf:"bytes,1,opt,name=forwardURL"` 27 | Body string `json:"body" protobuf:"bytes,2,opt,name=body"` 28 | Headers map[string][]string `json:"headers,omitempty" protobuf:"bytes,3,opt,name=headers"` 29 | } 30 | 31 | // HookStatus is the status for a Hook resource. 32 | type HookStatus struct { 33 | Phase HookPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=PodPhase"` 34 | Attempts int `json:"attempts,omitempty" protobuf:"bytes,2,opt,name=attempts"` 35 | Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"` 36 | NoRetryBefore *metav1.Time `json:"noRetryBefore,omitempty" protobuf:"bytes,4,opt,name=noRetryBefore"` 37 | CompletedTimestamp *metav1.Time `json:"completedTimestamp,omitempty" protobuf:"bytes,5,opt,name=completedTimestamp"` 38 | } 39 | 40 | // HookStatusType is the status of a hook; usually success or failed at completion. 41 | type HookPhase string 42 | 43 | const ( 44 | // HookPhaseNone an hook step has not started yet. 45 | HookPhaseNone HookPhase = "" 46 | // HookPhasePending the hook currently being relayed. 47 | HookPhasePending HookPhase = "Pending" 48 | // HookPhaseStatus the hook has been relayed. 49 | HookPhaseSending HookPhase = "Sending" 50 | // HookPhaseStatus the hook has been relayed. 51 | HookPhaseSuccess HookPhase = "Success" 52 | // HookPhaseStatus the hook has failed to be relayed. 53 | HookPhaseFailed HookPhase = "Failed" 54 | ) 55 | 56 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 57 | 58 | // HookList is a list of TypeMeta resources. 59 | type HookList struct { 60 | metav1.TypeMeta `json:",inline"` 61 | metav1.ListMeta `json:"metadata"` 62 | 63 | Items []Hook `json:"items"` 64 | } 65 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/crds.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: hooks.captainhook.io 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | app: {{ template "captain-hook.fullname" . }} 9 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 10 | release: "{{ .Release.Name }}" 11 | heritage: "{{ .Release.Service }}" 12 | spec: 13 | group: captainhook.io 14 | versions: 15 | - name: v1alpha1 16 | # Each version can be enabled/disabled by Served flag. 17 | served: true 18 | # One and only one version must be marked as the storage version. 19 | storage: true 20 | # Schema 21 | schema: 22 | openAPIV3Schema: 23 | type: object 24 | properties: 25 | spec: 26 | type: object 27 | properties: 28 | forwardURL: 29 | type: string 30 | body: 31 | type: string 32 | headers: 33 | type: object 34 | additionalProperties: 35 | type: array 36 | items: 37 | type: string 38 | status: 39 | type: object 40 | properties: 41 | status: 42 | type: string 43 | attempts: 44 | type: integer 45 | message: 46 | type: string 47 | noRetryBefore: 48 | type: string 49 | completedTimestamp: 50 | type: string 51 | additionalPrinterColumns: 52 | - name: Status 53 | type: string 54 | jsonPath: .status.phase 55 | - name: Attempts 56 | type: integer 57 | description: The number of attempts the webhooks has had. 58 | jsonPath: .status.attempts 59 | - name: No Retry Before 60 | type: string 61 | description: Do not attempt to retry this webhook before. 62 | jsonPath: .status.noRetryBefore 63 | priority: 1 64 | - name: Completed 65 | type: date 66 | description: When this hook was successfully sent. 67 | jsonPath: .status.completedTimestamp 68 | priority: 1 69 | - name: Age 70 | type: date 71 | jsonPath: .metadata.creationTimestamp 72 | - name: Error 73 | type: string 74 | jsonPath: .status.message 75 | priority: 1 76 | scope: Namespaced 77 | names: 78 | kind: Hook 79 | listKind: HookList 80 | plural: hooks 81 | shortNames: 82 | - hook 83 | singular: hook 84 | -------------------------------------------------------------------------------- /charts/captain-hook/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "captain-hook.fullname" . }} 6 | labels: 7 | {{- include "captain-hook.labels" . | nindent 4 }} 8 | spec: 9 | {{- if not .Values.autoscaling.enabled }} 10 | replicas: {{ .Values.replicaCount }} 11 | {{- end }} 12 | selector: 13 | matchLabels: 14 | {{- include "captain-hook.selectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | {{- with .Values.podAnnotations }} 18 | annotations: 19 | {{- toYaml . | nindent 8 }} 20 | {{- end }} 21 | labels: 22 | {{- include "captain-hook.selectorLabels" . | nindent 8 }} 23 | spec: 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | serviceAccountName: {{ include "captain-hook.serviceAccountName" . }} 29 | securityContext: 30 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 31 | containers: 32 | - name: {{ .Chart.Name }} 33 | securityContext: 34 | {{- toYaml .Values.securityContext | nindent 12 }} 35 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 36 | imagePullPolicy: {{ .Values.image.pullPolicy }} 37 | ports: 38 | - name: http 39 | containerPort: 8080 40 | protocol: TCP 41 | livenessProbe: 42 | httpGet: 43 | path: /health 44 | port: http 45 | readinessProbe: 46 | httpGet: 47 | path: /health 48 | port: http 49 | resources: 50 | {{- toYaml .Values.resources | nindent 12 }} 51 | env: 52 | - name: HOOK_PATH 53 | value: {{ .Values.hookPath }} 54 | - name: FORWARD_URL 55 | value: {{ .Values.forwardURL }} 56 | - name: INSECURE_RELAY 57 | value: {{ .Values.insecureRelay | quote }} 58 | - name: MAX_AGE_IN_SECONDS 59 | value: {{ .Values.maxAgeInSeconds | quote }} 60 | - name: ATTEMPT_RETRY_AFTER_IN_SECONDS 61 | value: {{ .Values.attemptRetryAfterInSeconds | quote }} 62 | - name: MAX_ATTEMPTS 63 | value: {{ .Values.maxAttempts | quote }} 64 | {{- with .Values.nodeSelector }} 65 | nodeSelector: 66 | {{- toYaml . | nindent 8 }} 67 | {{- end }} 68 | {{- with .Values.affinity }} 69 | affinity: 70 | {{- toYaml . | nindent 8 }} 71 | {{- end }} 72 | {{- with .Values.tolerations }} 73 | tolerations: 74 | {{- toYaml . | nindent 8 }} 75 | {{- end }} 76 | -------------------------------------------------------------------------------- /pkg/cmd/listen.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/jenkins-infra/captain-hook/pkg/hook" 12 | "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // ListenCmd defines the cmd. 17 | type ListenCmd struct { 18 | Cmd *cobra.Command 19 | Args []string 20 | 21 | ForwardURL string 22 | } 23 | 24 | // NewListenCmd defines a new cmd. 25 | func NewListenCmd() *cobra.Command { 26 | c := &ListenCmd{} 27 | cmd := &cobra.Command{ 28 | Use: "listen", 29 | Short: "captain-hook listen", 30 | Long: ``, 31 | Example: "", 32 | Run: func(cmd *cobra.Command, args []string) { 33 | c.Cmd = cmd 34 | c.Args = args 35 | err := c.Run() 36 | if err != nil { 37 | logrus.Errorf("unhandled error - %s", err) 38 | logrus.Fatal("unable to run command") 39 | } 40 | }, 41 | } 42 | 43 | c.Cmd = cmd 44 | 45 | cmd.Flags().StringVarP(&c.ForwardURL, "forward-url", "f", "http://jenkins/", 46 | "URL to forward webhooks to") 47 | 48 | return cmd 49 | } 50 | 51 | // Run update help. 52 | func (c *ListenCmd) Run() error { 53 | wait := 15 * time.Second 54 | options, err := hook.NewHook() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | go func() { 60 | if err = options.Start(); err != nil { 61 | logrus.Fatal(err) 62 | } 63 | }() 64 | 65 | // create a new router 66 | r := mux.NewRouter() 67 | 68 | // add routes 69 | options.Handle(r) 70 | 71 | srv := &http.Server{ 72 | Addr: "0.0.0.0:8080", 73 | // Good practice to set timeouts to avoid Slowloris attacks. 74 | WriteTimeout: time.Second * 15, 75 | ReadTimeout: time.Second * 15, 76 | IdleTimeout: time.Second * 60, 77 | Handler: r, // Pass our instance of gorilla/mux in. 78 | } 79 | 80 | go func() { 81 | if err := srv.ListenAndServe(); err != nil { 82 | logrus.Fatal(err) 83 | } 84 | }() 85 | 86 | channel := make(chan os.Signal, 1) 87 | // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) 88 | // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. 89 | signal.Notify(channel, os.Interrupt) 90 | 91 | // Block until we receive our signal. 92 | <-channel 93 | 94 | // Create a deadline to wait for. 95 | ctx, cancel := context.WithTimeout(context.Background(), wait) 96 | defer cancel() 97 | // Doesn't block if no connections, but will otherwise wait 98 | // until the timeout deadline. 99 | err = srv.Shutdown(ctx) 100 | if err != nil { 101 | return err 102 | } 103 | // Optionally, you could run srv.Shutdown in a goroutine and block on 104 | // <-ctx.Done() if your application should wait for other services 105 | // to finalize based on context cancellation. 106 | logrus.Info("shutting down") 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /cmd/captain-hook/captain-hook.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/jenkins-infra/captain-hook/pkg/cmd" 12 | "github.com/jenkins-infra/captain-hook/pkg/version" 13 | 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/pflag" 16 | ) 17 | 18 | // Version is dynamically set by the toolchain or overridden by the Makefile. 19 | var Version = version.Version 20 | 21 | var Verbose bool 22 | 23 | // BuildDate is dynamically set at build time in the Makefile. 24 | var BuildDate = version.BuildDate 25 | 26 | var versionOutput = "" 27 | 28 | func init() { 29 | // log in json format 30 | logrus.SetFormatter(&logrus.JSONFormatter{}) 31 | 32 | if strings.Contains(Version, "dev") { 33 | if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" { 34 | Version = info.Main.Version 35 | } 36 | } 37 | Version = strings.TrimPrefix(Version, "v") 38 | if BuildDate == "" { 39 | RootCmd.Version = Version 40 | } else { 41 | RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate) 42 | } 43 | versionOutput = fmt.Sprintf("captain-hook version %s", RootCmd.Version) 44 | RootCmd.AddCommand(versionCmd) 45 | RootCmd.SetVersionTemplate(versionOutput) 46 | 47 | RootCmd.PersistentFlags().Bool("help", false, "Show help for command") 48 | RootCmd.PersistentFlags().BoolVarP(&Verbose, "debug", "v", false, "Debug Output") 49 | 50 | RootCmd.Flags().Bool("version", false, "Show version") 51 | 52 | RootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 53 | if err == pflag.ErrHelp { 54 | return err 55 | } 56 | return &FlagError{Err: err} 57 | }) 58 | 59 | RootCmd.AddCommand(cmd.NewListenCmd()) 60 | 61 | RootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { 62 | if Verbose { 63 | logrus.SetLevel(logrus.DebugLevel) 64 | } 65 | } 66 | } 67 | 68 | // FlagError is the kind of error raised in flag processing. 69 | type FlagError struct { 70 | Err error 71 | } 72 | 73 | // Error. 74 | func (fe FlagError) Error() string { 75 | return fe.Err.Error() 76 | } 77 | 78 | // Unwrap FlagError. 79 | func (fe FlagError) Unwrap() error { 80 | return fe.Err 81 | } 82 | 83 | // RootCmd is the entry point of command-line execution. 84 | var RootCmd = &cobra.Command{ 85 | Use: "captain-hook", 86 | Short: "Store and Forward Webhooks", 87 | Long: `a HA store & forward webhook handler for Jenkins webhook events.`, 88 | 89 | SilenceErrors: false, 90 | SilenceUsage: false, 91 | } 92 | 93 | var versionCmd = &cobra.Command{ 94 | Use: "version", 95 | Hidden: true, 96 | Run: func(cmd *cobra.Command, args []string) { 97 | fmt.Print(versionOutput) 98 | }, 99 | } 100 | 101 | func main() { 102 | if err := RootCmd.Execute(); err != nil { 103 | os.Exit(1) 104 | } 105 | 106 | os.Exit(0) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package versioned 8 | 9 | import ( 10 | "fmt" 11 | 12 | captainhookv1alpha1 "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/typed/captainhookio/v1alpha1" 13 | discovery "k8s.io/client-go/discovery" 14 | rest "k8s.io/client-go/rest" 15 | flowcontrol "k8s.io/client-go/util/flowcontrol" 16 | ) 17 | 18 | type Interface interface { 19 | Discovery() discovery.DiscoveryInterface 20 | CaptainhookV1alpha1() captainhookv1alpha1.CaptainhookV1alpha1Interface 21 | } 22 | 23 | // Clientset contains the clients for groups. Each group has exactly one 24 | // version included in a Clientset. 25 | type Clientset struct { 26 | *discovery.DiscoveryClient 27 | captainhookV1alpha1 *captainhookv1alpha1.CaptainhookV1alpha1Client 28 | } 29 | 30 | // CaptainhookV1alpha1 retrieves the CaptainhookV1alpha1Client 31 | func (c *Clientset) CaptainhookV1alpha1() captainhookv1alpha1.CaptainhookV1alpha1Interface { 32 | return c.captainhookV1alpha1 33 | } 34 | 35 | // Discovery retrieves the DiscoveryClient 36 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 37 | if c == nil { 38 | return nil 39 | } 40 | return c.DiscoveryClient 41 | } 42 | 43 | // NewForConfig creates a new Clientset for the given config. 44 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 45 | // NewForConfig will generate a rate-limiter in configShallowCopy. 46 | func NewForConfig(c *rest.Config) (*Clientset, error) { 47 | configShallowCopy := *c 48 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 49 | if configShallowCopy.Burst <= 0 { 50 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 51 | } 52 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 53 | } 54 | var cs Clientset 55 | var err error 56 | cs.captainhookV1alpha1, err = captainhookv1alpha1.NewForConfig(&configShallowCopy) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &cs, nil 66 | } 67 | 68 | // NewForConfigOrDie creates a new Clientset for the given config and 69 | // panics if there is an error in the config. 70 | func NewForConfigOrDie(c *rest.Config) *Clientset { 71 | var cs Clientset 72 | cs.captainhookV1alpha1 = captainhookv1alpha1.NewForConfigOrDie(c) 73 | 74 | cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) 75 | return &cs 76 | } 77 | 78 | // New creates a new Clientset for the given RESTClient. 79 | func New(c rest.Interface) *Clientset { 80 | var cs Clientset 81 | cs.captainhookV1alpha1 = captainhookv1alpha1.New(c) 82 | 83 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 84 | return &cs 85 | } 86 | -------------------------------------------------------------------------------- /pkg/client/listers/captainhookio/v1alpha1/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by lister-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 11 | "k8s.io/apimachinery/pkg/api/errors" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/client-go/tools/cache" 14 | ) 15 | 16 | // HookLister helps list Hooks. 17 | // All objects returned here must be treated as read-only. 18 | type HookLister interface { 19 | // List lists all Hooks in the indexer. 20 | // Objects returned here must be treated as read-only. 21 | List(selector labels.Selector) (ret []*v1alpha1.Hook, err error) 22 | // Hooks returns an object that can list and get Hooks. 23 | Hooks(namespace string) HookNamespaceLister 24 | HookListerExpansion 25 | } 26 | 27 | // hookLister implements the HookLister interface. 28 | type hookLister struct { 29 | indexer cache.Indexer 30 | } 31 | 32 | // NewHookLister returns a new HookLister. 33 | func NewHookLister(indexer cache.Indexer) HookLister { 34 | return &hookLister{indexer: indexer} 35 | } 36 | 37 | // List lists all Hooks in the indexer. 38 | func (s *hookLister) List(selector labels.Selector) (ret []*v1alpha1.Hook, err error) { 39 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 40 | ret = append(ret, m.(*v1alpha1.Hook)) 41 | }) 42 | return ret, err 43 | } 44 | 45 | // Hooks returns an object that can list and get Hooks. 46 | func (s *hookLister) Hooks(namespace string) HookNamespaceLister { 47 | return hookNamespaceLister{indexer: s.indexer, namespace: namespace} 48 | } 49 | 50 | // HookNamespaceLister helps list and get Hooks. 51 | // All objects returned here must be treated as read-only. 52 | type HookNamespaceLister interface { 53 | // List lists all Hooks in the indexer for a given namespace. 54 | // Objects returned here must be treated as read-only. 55 | List(selector labels.Selector) (ret []*v1alpha1.Hook, err error) 56 | // Get retrieves the Hook from the indexer for a given namespace and name. 57 | // Objects returned here must be treated as read-only. 58 | Get(name string) (*v1alpha1.Hook, error) 59 | HookNamespaceListerExpansion 60 | } 61 | 62 | // hookNamespaceLister implements the HookNamespaceLister 63 | // interface. 64 | type hookNamespaceLister struct { 65 | indexer cache.Indexer 66 | namespace string 67 | } 68 | 69 | // List lists all Hooks in the indexer for a given namespace. 70 | func (s hookNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Hook, err error) { 71 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 72 | ret = append(ret, m.(*v1alpha1.Hook)) 73 | }) 74 | return ret, err 75 | } 76 | 77 | // Get retrieves the Hook from the indexer for a given namespace and name. 78 | func (s hookNamespaceLister) Get(name string) (*v1alpha1.Hook, error) { 79 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 80 | if err != nil { 81 | return nil, err 82 | } 83 | if !exists { 84 | return nil, errors.NewNotFound(v1alpha1.Resource("hook"), name) 85 | } 86 | return obj.(*v1alpha1.Hook), nil 87 | } 88 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/captainhookio/v1alpha1/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by informer-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "context" 11 | time "time" 12 | 13 | captainhookiov1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 14 | versioned "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned" 15 | internalinterfaces "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions/internalinterfaces" 16 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/client/listers/captainhookio/v1alpha1" 17 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | runtime "k8s.io/apimachinery/pkg/runtime" 19 | watch "k8s.io/apimachinery/pkg/watch" 20 | cache "k8s.io/client-go/tools/cache" 21 | ) 22 | 23 | // HookInformer provides access to a shared informer and lister for 24 | // Hooks. 25 | type HookInformer interface { 26 | Informer() cache.SharedIndexInformer 27 | Lister() v1alpha1.HookLister 28 | } 29 | 30 | type hookInformer struct { 31 | factory internalinterfaces.SharedInformerFactory 32 | tweakListOptions internalinterfaces.TweakListOptionsFunc 33 | namespace string 34 | } 35 | 36 | // NewHookInformer constructs a new informer for Hook type. 37 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 38 | // one. This reduces memory footprint and number of connections to the server. 39 | func NewHookInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 40 | return NewFilteredHookInformer(client, namespace, resyncPeriod, indexers, nil) 41 | } 42 | 43 | // NewFilteredHookInformer constructs a new informer for Hook type. 44 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 45 | // one. This reduces memory footprint and number of connections to the server. 46 | func NewFilteredHookInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 47 | return cache.NewSharedIndexInformer( 48 | &cache.ListWatch{ 49 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 50 | if tweakListOptions != nil { 51 | tweakListOptions(&options) 52 | } 53 | return client.CaptainhookV1alpha1().Hooks(namespace).List(context.TODO(), options) 54 | }, 55 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 56 | if tweakListOptions != nil { 57 | tweakListOptions(&options) 58 | } 59 | return client.CaptainhookV1alpha1().Hooks(namespace).Watch(context.TODO(), options) 60 | }, 61 | }, 62 | &captainhookiov1alpha1.Hook{}, 63 | resyncPeriod, 64 | indexers, 65 | ) 66 | } 67 | 68 | func (f *hookInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 69 | return NewFilteredHookInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 70 | } 71 | 72 | func (f *hookInformer) Informer() cache.SharedIndexInformer { 73 | return f.factory.InformerFor(&captainhookiov1alpha1.Hook{}, f.defaultInformer) 74 | } 75 | 76 | func (f *hookInformer) Lister() v1alpha1.HookLister { 77 | return v1alpha1.NewHookLister(f.Informer().GetIndexer()) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/store/kubernetes_store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned" 8 | 9 | v1alpha12 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 10 | "github.com/jenkins-infra/captain-hook/pkg/util" 11 | "github.com/sirupsen/logrus" 12 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/rest" 14 | ) 15 | 16 | var _ Store = &kubernetesStore{} 17 | 18 | type kubernetesStore struct { 19 | namespace string 20 | client versioned.Interface 21 | } 22 | 23 | func NewKubernetesStore() Store { 24 | config, err := rest.InClusterConfig() 25 | if err != nil { 26 | panic(err) 27 | } 28 | client, err := versioned.NewForConfig(config) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | namespace, err := util.Namespace() 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | return &kubernetesStore{client: client, namespace: namespace} 39 | } 40 | 41 | func (s *kubernetesStore) StoreHook(forwardURL string, body []byte, header map[string][]string) (string, error) { 42 | hook := v1alpha12.Hook{ 43 | ObjectMeta: v1.ObjectMeta{ 44 | GenerateName: "hook-", 45 | }, 46 | Spec: v1alpha12.HookSpec{ 47 | ForwardURL: forwardURL, 48 | Body: string(body), 49 | Headers: header, 50 | }, 51 | Status: v1alpha12.HookStatus{ 52 | Phase: v1alpha12.HookPhasePending, 53 | }, 54 | } 55 | 56 | created, err := s.client.CaptainhookV1alpha1().Hooks(s.namespace).Create(context.TODO(), &hook, v1.CreateOptions{}) 57 | if err != nil { 58 | return "", err 59 | } 60 | 61 | logrus.Debugf("persisted hook %+v", created) 62 | 63 | return created.ObjectMeta.Name, nil 64 | } 65 | 66 | func (s *kubernetesStore) Success(id string) error { 67 | hook, err := s.client.CaptainhookV1alpha1().Hooks(s.namespace).Get(context.TODO(), id, v1.GetOptions{}) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | hook.Status.Phase = v1alpha12.HookPhaseSuccess 73 | hook.Status.Message = "" 74 | now := v1.Now() 75 | hook.Status.CompletedTimestamp = &now 76 | 77 | _, err = s.client.CaptainhookV1alpha1().Hooks(s.namespace).Update(context.TODO(), hook, v1.UpdateOptions{}) 78 | if err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | func (s *kubernetesStore) Error(id string, message string) error { 85 | hook, err := s.client.CaptainhookV1alpha1().Hooks(s.namespace).Get(context.TODO(), id, v1.GetOptions{}) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | hook.Status.Phase = v1alpha12.HookPhaseFailed 91 | hook.Status.Message = message 92 | 93 | // FIXME need to add the correct time here 94 | retry := v1.NewTime(time.Now().Add(time.Minute * 1)) 95 | hook.Status.NoRetryBefore = &retry 96 | 97 | _, err = s.client.CaptainhookV1alpha1().Hooks(s.namespace).Update(context.TODO(), hook, v1.UpdateOptions{}) 98 | if err != nil { 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | func (s *kubernetesStore) Delete(id string) error { 105 | return s.client.CaptainhookV1alpha1().Hooks(s.namespace).Delete(context.TODO(), id, v1.DeleteOptions{}) 106 | } 107 | 108 | func (s *kubernetesStore) MarkForRetry(id string) error { 109 | hook, err := s.client.CaptainhookV1alpha1().Hooks(s.namespace).Get(context.TODO(), id, v1.GetOptions{}) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | hook.Status.Phase = v1alpha12.HookPhasePending 115 | hook.Status.Message = "" 116 | hook.Status.Attempts = hook.Status.Attempts + 1 117 | hook.Status.NoRetryBefore = nil 118 | 119 | _, err = s.client.CaptainhookV1alpha1().Hooks(s.namespace).Update(context.TODO(), hook, v1.UpdateOptions{}) 120 | if err != nil { 121 | return err 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /hack/generate-groups.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2017 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | # generate-groups generates everything for a project with external types only, e.g. a project based 22 | # on CustomResourceDefinitions. 23 | 24 | if [ "$#" -lt 4 ] || [ "${1}" == "--help" ]; then 25 | cat < ... 27 | 28 | the generators comma separated to run (deepcopy,defaulter,client,lister,informer) or "all". 29 | the output package name (e.g. github.com/example/project/pkg/generated). 30 | the external types dir (e.g. github.com/example/api or github.com/example/project/pkg/apis). 31 | the groups and their versions in the format "groupA:v1,v2 groupB:v1 groupC:v2", relative 32 | to . 33 | ... arbitrary flags passed to all generator binaries. 34 | 35 | 36 | Examples: 37 | $(basename "$0") all github.com/example/project/pkg/client github.com/example/project/pkg/apis "foo:v1 bar:v1alpha1,v1beta1" 38 | $(basename "$0") deepcopy,client github.com/example/project/pkg/client github.com/example/project/pkg/apis "foo:v1 bar:v1alpha1,v1beta1" 39 | EOF 40 | exit 0 41 | fi 42 | 43 | GENS="$1" 44 | OUTPUT_PKG="$2" 45 | APIS_PKG="$3" 46 | GROUPS_WITH_VERSIONS="$4" 47 | shift 4 48 | 49 | function codegen::join() { local IFS="$1"; shift; echo "$*"; } 50 | 51 | # enumerate group versions 52 | FQ_APIS=() # e.g. k8s.io/api/apps/v1 53 | for GVs in ${GROUPS_WITH_VERSIONS}; do 54 | IFS=: read -r G Vs <<<"${GVs}" 55 | 56 | # enumerate versions 57 | for V in ${Vs//,/ }; do 58 | FQ_APIS+=("${APIS_PKG}/${G}/${V}") 59 | done 60 | done 61 | 62 | if [ "${GENS}" = "all" ] || grep -qw "deepcopy" <<<"${GENS}"; then 63 | echo "Generating deepcopy funcs" 64 | "deepcopy-gen" --input-dirs "$(codegen::join , "${FQ_APIS[@]}")" -O zz_generated.deepcopy --bounding-dirs "${APIS_PKG}" "$@" 65 | fi 66 | 67 | if [ "${GENS}" = "all" ] || grep -qw "client" <<<"${GENS}"; then 68 | echo "Generating clientset for ${GROUPS_WITH_VERSIONS} at ${OUTPUT_PKG}/${CLIENTSET_PKG_NAME:-clientset}" 69 | "client-gen" --clientset-name "${CLIENTSET_NAME_VERSIONED:-versioned}" --input-base "" --input "$(codegen::join , "${FQ_APIS[@]}")" --output-package "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME:-clientset}" "$@" 70 | fi 71 | 72 | if [ "${GENS}" = "all" ] || grep -qw "lister" <<<"${GENS}"; then 73 | echo "Generating listers for ${GROUPS_WITH_VERSIONS} at ${OUTPUT_PKG}/listers" 74 | "lister-gen" --input-dirs "$(codegen::join , "${FQ_APIS[@]}")" --output-package "${OUTPUT_PKG}/listers" "$@" 75 | fi 76 | 77 | if [ "${GENS}" = "all" ] || grep -qw "informer" <<<"${GENS}"; then 78 | echo "Generating informers for ${GROUPS_WITH_VERSIONS} at ${OUTPUT_PKG}/informers" 79 | "informer-gen" \ 80 | --input-dirs "$(codegen::join , "${FQ_APIS[@]}")" \ 81 | --versioned-clientset-package "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME:-clientset}/${CLIENTSET_NAME_VERSIONED:-versioned}" \ 82 | --listers-package "${OUTPUT_PKG}/listers" \ 83 | --output-package "${OUTPUT_PKG}/informers" \ 84 | "$@" 85 | fi -------------------------------------------------------------------------------- /pkg/api/captainhookio/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | Generated Code 5 | */ 6 | 7 | // Code generated by deepcopy-gen. DO NOT EDIT. 8 | 9 | package v1alpha1 10 | 11 | import ( 12 | runtime "k8s.io/apimachinery/pkg/runtime" 13 | ) 14 | 15 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 16 | func (in *Hook) DeepCopyInto(out *Hook) { 17 | *out = *in 18 | out.TypeMeta = in.TypeMeta 19 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 20 | in.Spec.DeepCopyInto(&out.Spec) 21 | in.Status.DeepCopyInto(&out.Status) 22 | return 23 | } 24 | 25 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hook. 26 | func (in *Hook) DeepCopy() *Hook { 27 | if in == nil { 28 | return nil 29 | } 30 | out := new(Hook) 31 | in.DeepCopyInto(out) 32 | return out 33 | } 34 | 35 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 36 | func (in *Hook) DeepCopyObject() runtime.Object { 37 | if c := in.DeepCopy(); c != nil { 38 | return c 39 | } 40 | return nil 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in *HookList) DeepCopyInto(out *HookList) { 45 | *out = *in 46 | out.TypeMeta = in.TypeMeta 47 | in.ListMeta.DeepCopyInto(&out.ListMeta) 48 | if in.Items != nil { 49 | in, out := &in.Items, &out.Items 50 | *out = make([]Hook, len(*in)) 51 | for i := range *in { 52 | (*in)[i].DeepCopyInto(&(*out)[i]) 53 | } 54 | } 55 | return 56 | } 57 | 58 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HookList. 59 | func (in *HookList) DeepCopy() *HookList { 60 | if in == nil { 61 | return nil 62 | } 63 | out := new(HookList) 64 | in.DeepCopyInto(out) 65 | return out 66 | } 67 | 68 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 69 | func (in *HookList) DeepCopyObject() runtime.Object { 70 | if c := in.DeepCopy(); c != nil { 71 | return c 72 | } 73 | return nil 74 | } 75 | 76 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 77 | func (in *HookSpec) DeepCopyInto(out *HookSpec) { 78 | *out = *in 79 | if in.Headers != nil { 80 | in, out := &in.Headers, &out.Headers 81 | *out = make(map[string][]string, len(*in)) 82 | for key, val := range *in { 83 | var outVal []string 84 | if val == nil { 85 | (*out)[key] = nil 86 | } else { 87 | in, out := &val, &outVal 88 | *out = make([]string, len(*in)) 89 | copy(*out, *in) 90 | } 91 | (*out)[key] = outVal 92 | } 93 | } 94 | return 95 | } 96 | 97 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HookSpec. 98 | func (in *HookSpec) DeepCopy() *HookSpec { 99 | if in == nil { 100 | return nil 101 | } 102 | out := new(HookSpec) 103 | in.DeepCopyInto(out) 104 | return out 105 | } 106 | 107 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 108 | func (in *HookStatus) DeepCopyInto(out *HookStatus) { 109 | *out = *in 110 | if in.NoRetryBefore != nil { 111 | in, out := &in.NoRetryBefore, &out.NoRetryBefore 112 | *out = (*in).DeepCopy() 113 | } 114 | if in.CompletedTimestamp != nil { 115 | in, out := &in.CompletedTimestamp, &out.CompletedTimestamp 116 | *out = (*in).DeepCopy() 117 | } 118 | return 119 | } 120 | 121 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HookStatus. 122 | func (in *HookStatus) DeepCopy() *HookStatus { 123 | if in == nil { 124 | return nil 125 | } 126 | out := new(HookStatus) 127 | in.DeepCopyInto(out) 128 | return out 129 | } 130 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters-settings: 3 | depguard: 4 | list-type: blacklist 5 | packages: 6 | - github.com/alecthomas/assert 7 | - github.com/magiconair/properties/assert 8 | packages-with-error-message: 9 | - github.com/alecthomas/assert: "use github.com/stretchr/testify/assert" 10 | - github.com/magiconair/properties/assert: "use github.com/stretchr/testify/assert" 11 | dupl: 12 | threshold: 100 13 | exhaustive: 14 | default-signifies-exhaustive: false 15 | funlen: 16 | lines: 200 17 | statements: 150 18 | goconst: 19 | min-len: 3 20 | min-occurrences: 3 21 | gocritic: 22 | enabled-tags: 23 | - diagnostic 24 | - experimental 25 | - opinionated 26 | - performance 27 | - style 28 | disabled-checks: 29 | - dupImport # https://github.com/go-critic/go-critic/issues/845 30 | - ifElseChain 31 | - octalLiteral 32 | - whyNoLint 33 | - wrapperFunc 34 | gocyclo: 35 | min-complexity: 30 36 | goimports: 37 | golint: 38 | min-confidence: 0 39 | gomnd: 40 | settings: 41 | mnd: 42 | # don't include the "operation" and "assign" 43 | checks: argument,case,condition,return 44 | govet: 45 | check-shadowing: true 46 | settings: 47 | printf: 48 | funcs: 49 | - (github.com/jenkins-x/jx-logging/pkg/log/Logger()).Debugf 50 | - (github.com/jenkins-x/jx-logging/pkg/log/Logger()).Infof 51 | - (github.com/jenkins-x/jx-logging/pkg/log/Logger()).Warnf 52 | - (github.com/jenkins-x/jx-logging/pkg/log/Logger()).Errorf 53 | - (github.com/jenkins-x/jx-logging/pkg/log/Logger()).Fatalf 54 | lll: 55 | line-length: 140 56 | maligned: 57 | suggest-new: true 58 | misspell: 59 | nolintlint: 60 | allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) 61 | allow-unused: false # report any unused nolint directives 62 | require-explanation: false # don't require an explanation for nolint directives 63 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 64 | linters: 65 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 66 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 67 | disable-all: true 68 | enable: 69 | - asciicheck 70 | - deadcode 71 | - depguard 72 | - dogsled 73 | - errcheck 74 | - funlen 75 | - gocognit 76 | - goconst 77 | - gocyclo 78 | - godot 79 | - gofmt 80 | - goimports 81 | - golint 82 | - goprintffuncname 83 | - gosec 84 | - gosimple 85 | - ineffassign 86 | - interfacer 87 | - maligned 88 | - misspell 89 | - nakedret 90 | - nolintlint 91 | - rowserrcheck 92 | - structcheck 93 | - stylecheck 94 | - typecheck 95 | - unconvert 96 | - unparam 97 | - unused 98 | - varcheck 99 | - whitespace 100 | # - testpackage 101 | # don't enable: 102 | # - bodyclose https://github.com/timakin/bodyclose/issues/30 103 | # - gochecknoinits 104 | # - gocritic 105 | # - dupl 106 | # - lll 107 | # - govet 108 | # - exhaustive (TODO: enable after next release; current release at time of writing is v1.27) 109 | # - gochecknoglobals 110 | # - godox 111 | # - goerr113 112 | # - nestif 113 | # - staticcheck 114 | # - prealloc 115 | # - wsl 116 | # - gomnd 117 | # - scopelint 118 | issues: 119 | # Excluding configuration per-path, per-linter, per-text and per-source 120 | exclude-rules: 121 | - path: _test\.go 122 | linters: 123 | - gomnd 124 | - path: _expansion_test\.go 125 | linters: 126 | - testpackage 127 | # https://github.com/go-critic/go-critic/issues/926 128 | - linters: 129 | - gocritic 130 | text: "unnecessaryDefer:" 131 | run: 132 | skip-dirs: 133 | - pkg/client 134 | skip-files: 135 | - pkg/api/captainhookio/v1alpha1/zz_generated.deepcopy.go 136 | # golangci.com configuration 137 | # https://github.com/golangci/golangci/wiki/Configuration 138 | service: 139 | golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly 140 | prepare: 141 | - echo "here I can run custom commands, but no preparation needed for this repo" 142 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.16 18 | - name: Install Helm 19 | uses: azure/setup-helm@18bc76811624f360dbd7f18c2d4ecb32c7b87bab # v1 20 | with: 21 | version: v3.5.2 22 | - name: Prepare 23 | id: prep 24 | run: | 25 | DOCKER_IMAGE=jenkinsciinfra/captain-hook 26 | VERSION=latest 27 | GORELEASER_ARGS="build --rm-dist --snapshot" 28 | RELEASE_CHART=false 29 | 30 | if [[ $GITHUB_REF == refs/tags/* ]]; then 31 | # release 32 | VERSION=${GITHUB_REF#refs/tags/} 33 | GORELEASER_ARGS="release --rm-dist" 34 | RELEASE_CHART=true 35 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 36 | # branch 37 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 38 | if [[ $VERSION == 'main' ]]; then 39 | VERSION=latest 40 | fi 41 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 42 | # pr 43 | VERSION=pr-${{ github.event.number }} 44 | fi 45 | 46 | TAGS="${DOCKER_IMAGE}:${VERSION}" 47 | 48 | echo ::set-output name=release_chart::${RELEASE_CHART} 49 | echo ::set-output name=goreleaser_args::${GORELEASER_ARGS} 50 | echo ::set-output name=image::${DOCKER_IMAGE} 51 | echo ::set-output name=version::${VERSION} 52 | echo ::set-output name=tags::${TAGS} 53 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 54 | - name: Run GoReleaser 55 | uses: goreleaser/goreleaser-action@56f5b77f7fa4a8fe068bf22b732ec036cc9bc13f # v2.4.1 56 | with: 57 | version: latest 58 | args: ${{ steps.prep.outputs.goreleaser_args }} 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | - name: Set up QEMU 62 | uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1 63 | - name: Set up Docker Buildx 64 | uses: docker/setup-buildx-action@f211e3e9ded2d9377c8cadc4489a4e38014bc4c9 # v1 65 | - name: Login to DockerHub 66 | uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 # v1 67 | with: 68 | username: ${{ secrets.DOCKERHUB_USERNAME }} 69 | password: ${{ secrets.DOCKERHUB_TOKEN }} 70 | - name: Build and push 71 | id: docker_build 72 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a # v2 73 | with: 74 | context: . 75 | push: true 76 | tags: ${{ steps.prep.outputs.tags }} 77 | platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le 78 | labels: | 79 | org.opencontainers.image.source=${{ github.event.repository.html_url }} 80 | org.opencontainers.image.url=${{ github.event.repository.html_url }} 81 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 82 | org.opencontainers.image.revision=${{ github.sha }} 83 | - name: Configure Git 84 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 85 | run: | 86 | git config user.name "$GITHUB_ACTOR" 87 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 88 | - name: Update appVersion in Chart.yaml 89 | uses: mikefarah/yq@111c6e0be18ffde03c9fd51066f5eed5d12f0703 # v4.6.0 90 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 91 | with: 92 | cmd: yq eval '.appVersion = "${{ steps.prep.outputs.version }}"' -i charts/captain-hook/Chart.yaml 93 | - name: Update version in Chart.yaml 94 | uses: mikefarah/yq@111c6e0be18ffde03c9fd51066f5eed5d12f0703 # v4.6.0 95 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 96 | with: 97 | cmd: yq eval '.version = "${{ steps.prep.outputs.version }}"' -i charts/captain-hook/Chart.yaml 98 | - name: Install Helm 99 | uses: azure/setup-helm@18bc76811624f360dbd7f18c2d4ecb32c7b87bab # v1 100 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 101 | with: 102 | version: v3.5.2 103 | -------------------------------------------------------------------------------- /pkg/hook/handler.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/jenkins-infra/captain-hook/pkg/store" 13 | 14 | "github.com/gorilla/mux" 15 | "github.com/jenkins-infra/captain-hook/pkg/version" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | HealthPath = "/health" 21 | ) 22 | 23 | // Options struct containing all options. 24 | type Options struct { 25 | Path string 26 | Version string 27 | ForwardURL string 28 | handler *handler 29 | informer *informer 30 | } 31 | 32 | // NewHook create a new hook handler. 33 | func NewHook() (*Options, error) { 34 | logrus.Infof("creating new webhook listener") 35 | 36 | store := store.NewKubernetesStore() 37 | sender := NewSender() 38 | 39 | return &Options{ 40 | Path: os.Getenv("HOOK_PATH"), 41 | Version: version.Version, 42 | ForwardURL: os.Getenv("FORWARD_URL"), 43 | handler: &handler{ 44 | store: store, 45 | sender: sender, 46 | }, 47 | informer: &informer{ 48 | maxAgeInSeconds: Atoi(os.Getenv("MAX_AGE_IN_SECONDS")), 49 | store: store, 50 | sender: sender, 51 | }, 52 | }, nil 53 | } 54 | 55 | func (o *Options) Start() error { 56 | return o.informer.Start() 57 | } 58 | 59 | func (o *Options) Handle(mux *mux.Router) { 60 | logrus.Infof("Handling health on %s", HealthPath) 61 | mux.Handle(HealthPath, http.HandlerFunc(o.health)) 62 | 63 | mux.Handle("/", http.HandlerFunc(o.defaultHandler)) 64 | 65 | logrus.Infof("Handling hook on %s", o.Path) 66 | mux.Handle(o.Path, http.HandlerFunc(o.handleWebHookRequests)) 67 | } 68 | 69 | // health returns either HTTP 204 if the service is healthy, otherwise nothing ('cos it's dead). 70 | func (o *Options) health(w http.ResponseWriter, r *http.Request) { 71 | logrus.Trace("Health check") 72 | w.WriteHeader(http.StatusNoContent) 73 | } 74 | 75 | func (o *Options) defaultHandler(w http.ResponseWriter, r *http.Request) { 76 | path := r.URL.Path 77 | if path == o.Path || strings.HasPrefix(path, o.Path+"/") { 78 | o.handleWebHookRequests(w, r) 79 | return 80 | } 81 | path = strings.TrimPrefix(path, "/") 82 | if path == "" || path == "index.html" { 83 | o.getIndex(w) 84 | return 85 | } 86 | http.Error(w, fmt.Sprintf("unknown path %s", path), 404) 87 | } 88 | 89 | // getIndex returns a simple home page. 90 | func (o *Options) getIndex(w io.Writer) { 91 | logrus.Debug("GET index") 92 | message := "Captain Hook" 93 | 94 | _, err := w.Write([]byte(message)) 95 | if err != nil { 96 | logrus.Debugf("failed to write the index: %v", err) 97 | } 98 | } 99 | 100 | func (o *Options) handleWebHookRequests(w http.ResponseWriter, r *http.Request) { 101 | logrus.Infof("got incomming request") 102 | if r.Method != http.MethodPost { 103 | // liveness probe etc 104 | logrus.Info("invalid http method so returning index") 105 | o.getIndex(w) 106 | return 107 | } 108 | 109 | bodyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, 10000000)) 110 | if err != nil { 111 | logrus.Errorf("failed to Read Body: %s", err.Error()) 112 | responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: Read Body: %s", err.Error())) 113 | return 114 | } 115 | 116 | err = r.Body.Close() // must close 117 | if err != nil { 118 | logrus.Errorf("failed to Close Body: %s", err.Error()) 119 | responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: Read Close: %s", err.Error())) 120 | return 121 | } 122 | 123 | logrus.Debugf("got hook body %s", string(bodyBytes)) 124 | logrus.Debugf("got headers %s", r.Header) 125 | 126 | err = o.onGeneralHook(bodyBytes, r.Header) 127 | if err != nil { 128 | logrus.Errorf("failed to process webhook: %s", err) 129 | responseHTTPError(w, http.StatusInternalServerError, "500 Internal Server Error: %s", err.Error()) 130 | } 131 | 132 | writeResult(w, "OK") 133 | } 134 | 135 | func (o *Options) onGeneralHook(bodyBytes []byte, headers http.Header) error { 136 | hook := Hook{ 137 | ForwardURL: o.ForwardURL, 138 | Body: bodyBytes, 139 | Headers: headers, 140 | } 141 | 142 | err := o.handler.Handle(&hook) 143 | if err != nil { 144 | logrus.Errorf("failed to deliver webhook after %s", err) 145 | return err 146 | } 147 | 148 | logrus.Infof("webhook delivery ok for %s", hook.Name) 149 | 150 | return nil 151 | } 152 | 153 | func Atoi(in string) int { 154 | out, err := strconv.Atoi(in) 155 | if err != nil { 156 | panic(err) 157 | } 158 | return out 159 | } 160 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/fake/fake_hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package fake 8 | 9 | import ( 10 | "context" 11 | 12 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | labels "k8s.io/apimachinery/pkg/labels" 15 | schema "k8s.io/apimachinery/pkg/runtime/schema" 16 | types "k8s.io/apimachinery/pkg/types" 17 | watch "k8s.io/apimachinery/pkg/watch" 18 | testing "k8s.io/client-go/testing" 19 | ) 20 | 21 | // FakeHooks implements HookInterface 22 | type FakeHooks struct { 23 | Fake *FakeCaptainhookV1alpha1 24 | ns string 25 | } 26 | 27 | var hooksResource = schema.GroupVersionResource{Group: "captainhook.io", Version: "v1alpha1", Resource: "hooks"} 28 | 29 | var hooksKind = schema.GroupVersionKind{Group: "captainhook.io", Version: "v1alpha1", Kind: "Hook"} 30 | 31 | // Get takes name of the hook, and returns the corresponding hook object, and an error if there is any. 32 | func (c *FakeHooks) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Hook, err error) { 33 | obj, err := c.Fake. 34 | Invokes(testing.NewGetAction(hooksResource, c.ns, name), &v1alpha1.Hook{}) 35 | 36 | if obj == nil { 37 | return nil, err 38 | } 39 | return obj.(*v1alpha1.Hook), err 40 | } 41 | 42 | // List takes label and field selectors, and returns the list of Hooks that match those selectors. 43 | func (c *FakeHooks) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.HookList, err error) { 44 | obj, err := c.Fake. 45 | Invokes(testing.NewListAction(hooksResource, hooksKind, c.ns, opts), &v1alpha1.HookList{}) 46 | 47 | if obj == nil { 48 | return nil, err 49 | } 50 | 51 | label, _, _ := testing.ExtractFromListOptions(opts) 52 | if label == nil { 53 | label = labels.Everything() 54 | } 55 | list := &v1alpha1.HookList{ListMeta: obj.(*v1alpha1.HookList).ListMeta} 56 | for _, item := range obj.(*v1alpha1.HookList).Items { 57 | if label.Matches(labels.Set(item.Labels)) { 58 | list.Items = append(list.Items, item) 59 | } 60 | } 61 | return list, err 62 | } 63 | 64 | // Watch returns a watch.Interface that watches the requested hooks. 65 | func (c *FakeHooks) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 66 | return c.Fake. 67 | InvokesWatch(testing.NewWatchAction(hooksResource, c.ns, opts)) 68 | 69 | } 70 | 71 | // Create takes the representation of a hook and creates it. Returns the server's representation of the hook, and an error, if there is any. 72 | func (c *FakeHooks) Create(ctx context.Context, hook *v1alpha1.Hook, opts v1.CreateOptions) (result *v1alpha1.Hook, err error) { 73 | obj, err := c.Fake. 74 | Invokes(testing.NewCreateAction(hooksResource, c.ns, hook), &v1alpha1.Hook{}) 75 | 76 | if obj == nil { 77 | return nil, err 78 | } 79 | return obj.(*v1alpha1.Hook), err 80 | } 81 | 82 | // Update takes the representation of a hook and updates it. Returns the server's representation of the hook, and an error, if there is any. 83 | func (c *FakeHooks) Update(ctx context.Context, hook *v1alpha1.Hook, opts v1.UpdateOptions) (result *v1alpha1.Hook, err error) { 84 | obj, err := c.Fake. 85 | Invokes(testing.NewUpdateAction(hooksResource, c.ns, hook), &v1alpha1.Hook{}) 86 | 87 | if obj == nil { 88 | return nil, err 89 | } 90 | return obj.(*v1alpha1.Hook), err 91 | } 92 | 93 | // Delete takes name of the hook and deletes it. Returns an error if one occurs. 94 | func (c *FakeHooks) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 95 | _, err := c.Fake. 96 | Invokes(testing.NewDeleteAction(hooksResource, c.ns, name), &v1alpha1.Hook{}) 97 | 98 | return err 99 | } 100 | 101 | // DeleteCollection deletes a collection of objects. 102 | func (c *FakeHooks) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 103 | action := testing.NewDeleteCollectionAction(hooksResource, c.ns, listOpts) 104 | 105 | _, err := c.Fake.Invokes(action, &v1alpha1.HookList{}) 106 | return err 107 | } 108 | 109 | // Patch applies the patch and returns the patched hook. 110 | func (c *FakeHooks) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Hook, err error) { 111 | obj, err := c.Fake. 112 | Invokes(testing.NewPatchSubresourceAction(hooksResource, c.ns, name, pt, data, subresources...), &v1alpha1.Hook{}) 113 | 114 | if obj == nil { 115 | return nil, err 116 | } 117 | return obj.(*v1alpha1.Hook), err 118 | } 119 | -------------------------------------------------------------------------------- /pkg/hook/informer.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jenkins-infra/captain-hook/pkg/store" 7 | 8 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | 10 | v1alpha12 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 11 | "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned" 12 | "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions" 13 | "github.com/jenkins-infra/captain-hook/pkg/util" 14 | "github.com/sirupsen/logrus" 15 | "k8s.io/client-go/rest" 16 | "k8s.io/client-go/tools/cache" 17 | ) 18 | 19 | const ( 20 | resyncPeriod = time.Second * 30 21 | ) 22 | 23 | type informer struct { 24 | maxAgeInSeconds int 25 | client versioned.Interface 26 | namespace string 27 | sender Sender 28 | store store.Store 29 | } 30 | 31 | func (i *informer) Start() error { 32 | if i.client == nil { 33 | logrus.Infof("getting in cluster config") 34 | config, err := rest.InClusterConfig() 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | logrus.Infof("creating client set") 40 | i.client, err = versioned.NewForConfig(config) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | i.namespace, err = util.Namespace() 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | 51 | logrus.Infof("creating shared informer factory") 52 | factory := externalversions.NewSharedInformerFactoryWithOptions(i.client, resyncPeriod, externalversions.WithNamespace(i.namespace)) 53 | informer := factory.Captainhook().V1alpha1().Hooks().Informer() 54 | 55 | stopper := make(chan struct{}) 56 | 57 | defer close(stopper) 58 | informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 59 | AddFunc: func(obj interface{}) { 60 | h := obj.(*v1alpha12.Hook) 61 | logrus.Infof("Created hook in namespace %s, name %s at %s", h.ObjectMeta.Namespace, h.ObjectMeta.Name, h.ObjectMeta.CreationTimestamp) 62 | }, 63 | DeleteFunc: func(obj interface{}) { 64 | h := obj.(*v1alpha12.Hook) 65 | logrus.Infof("Deleted hook in namespace %s, name %s", h.ObjectMeta.Namespace, h.ObjectMeta.Name) 66 | }, 67 | UpdateFunc: func(oldObj interface{}, newObj interface{}) { 68 | h := newObj.(*v1alpha12.Hook) 69 | logrus.Infof("Updated hook in namespace %s, name %s at %s", h.ObjectMeta.Namespace, h.ObjectMeta.Name, h.ObjectMeta.CreationTimestamp) 70 | if h.Status.Phase == v1alpha12.HookPhaseSuccess { 71 | logrus.Infof("Hook %s is success, checking age... %s", h.ObjectMeta.Name, h.Status.CompletedTimestamp) 72 | err := i.DeleteIfOld(h) 73 | if err != nil { 74 | logrus.Errorf("delete if old failed: %s", err.Error()) 75 | } 76 | } else if h.Status.Phase == v1alpha12.HookPhaseFailed { 77 | now := v1.Now() 78 | retry := h.Status.NoRetryBefore 79 | logrus.Infof("checking if retry date %s is before %s", retry, now) 80 | if retry.Before(&now) { 81 | err := i.Retry(h) 82 | if err != nil { 83 | logrus.Errorf("retry failed: %s", err.Error()) 84 | } 85 | } 86 | } 87 | }, 88 | }) 89 | informer.Run(stopper) 90 | 91 | return nil 92 | } 93 | 94 | func (i *informer) DeleteIfOld(hook *v1alpha12.Hook) error { 95 | // if phase is successful 96 | if hook.Status.Phase == v1alpha12.HookPhaseSuccess { 97 | // and age is more than period set 98 | ts := v1.Now().Add(time.Second * time.Duration(-1*i.maxAgeInSeconds)) 99 | logrus.Infof("checking if hook %s is older than %s", hook.ObjectMeta.Name, ts) 100 | if ts.After(hook.Status.CompletedTimestamp.Time) { 101 | // then delete 102 | err := i.store.Delete(hook.ObjectMeta.Name) 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (i *informer) Retry(hook *v1alpha12.Hook) error { 113 | // if phase is failed or none 114 | if hook.Status.Phase == v1alpha12.HookPhaseFailed { 115 | logrus.Infof("retrying hook %s", hook.ObjectMeta.Name) 116 | err := i.store.MarkForRetry(hook.ObjectMeta.Name) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | // attempt to send 122 | logrus.Infof("resending hook %s", hook.ObjectMeta.Name) 123 | err = i.sender.send(hook.Spec.ForwardURL, []byte(hook.Spec.Body), hook.Spec.Headers) 124 | 125 | if err != nil { 126 | // mark as failed if errored 127 | logrus.Infof("recording hook %s as failed: %s", hook.ObjectMeta.Name, err.Error()) 128 | err = i.store.Error(hook.ObjectMeta.Name, err.Error()) 129 | if err != nil { 130 | return err 131 | } 132 | } else { 133 | // mark as success if passed 134 | logrus.Infof("recording hook %s as success", hook.ObjectMeta.Name) 135 | err = i.store.Success(hook.ObjectMeta.Name) 136 | if err != nil { 137 | return err 138 | } 139 | } 140 | } 141 | 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.16 18 | - name: Prepare 19 | id: prep 20 | run: | 21 | DOCKER_IMAGE=jenkinsciinfra/captain-hook 22 | VERSION=latest 23 | GORELEASER_ARGS="build --rm-dist --snapshot" 24 | RELEASE_CHART=false 25 | 26 | if [[ $GITHUB_REF == refs/tags/* ]]; then 27 | # release 28 | VERSION=${GITHUB_REF#refs/tags/} 29 | GORELEASER_ARGS="release --rm-dist" 30 | RELEASE_CHART=true 31 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 32 | # branch 33 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 34 | if [[ $VERSION == 'main' ]]; then 35 | VERSION=latest 36 | fi 37 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 38 | # pr 39 | VERSION=pr-${{ github.event.number }} 40 | fi 41 | 42 | TAGS="${DOCKER_IMAGE}:${VERSION}" 43 | 44 | echo ::set-output name=release_chart::${RELEASE_CHART} 45 | echo ::set-output name=goreleaser_args::${GORELEASER_ARGS} 46 | echo ::set-output name=image::${DOCKER_IMAGE} 47 | echo ::set-output name=version::${VERSION} 48 | echo ::set-output name=tags::${TAGS} 49 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 50 | - name: Run GoReleaser 51 | uses: goreleaser/goreleaser-action@56f5b77f7fa4a8fe068bf22b732ec036cc9bc13f # v2.4.1 52 | with: 53 | version: latest 54 | args: ${{ steps.prep.outputs.goreleaser_args }} 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | - name: Set up QEMU 58 | uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1 59 | - name: Set up Docker Buildx 60 | uses: docker/setup-buildx-action@f211e3e9ded2d9377c8cadc4489a4e38014bc4c9 # v1 61 | - name: Login to DockerHub 62 | uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 # v1 63 | with: 64 | username: ${{ secrets.DOCKERHUB_USERNAME }} 65 | password: ${{ secrets.DOCKERHUB_TOKEN }} 66 | - name: Build and push 67 | id: docker_build 68 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a # v2 69 | with: 70 | context: . 71 | push: true 72 | tags: ${{ steps.prep.outputs.tags }} 73 | platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le 74 | labels: | 75 | org.opencontainers.image.source=${{ github.event.repository.html_url }} 76 | org.opencontainers.image.url=${{ github.event.repository.html_url }} 77 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 78 | org.opencontainers.image.revision=${{ github.sha }} 79 | org.label-schema.vcs-url=${{ github.event.repository.html_url }} 80 | org.label-schema.url=${{ github.event.repository.html_url }} 81 | org.label-schema.build-date=${{ steps.prep.outputs.created }} 82 | org.label-schema.vcs-ref=${{ github.sha }} 83 | inspect.tree.state=clean 84 | - name: Configure Git 85 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 86 | run: | 87 | git config user.name "$GITHUB_ACTOR" 88 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 89 | - name: Update appVersion in Chart.yaml 90 | uses: mikefarah/yq@111c6e0be18ffde03c9fd51066f5eed5d12f0703 # v4.6.0 91 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 92 | with: 93 | cmd: yq eval '.appVersion = "${{ steps.prep.outputs.version }}"' -i charts/captain-hook/Chart.yaml 94 | - name: Update version in Chart.yaml 95 | uses: mikefarah/yq@111c6e0be18ffde03c9fd51066f5eed5d12f0703 # v4.6.0 96 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 97 | with: 98 | cmd: yq eval '.version = "${{ steps.prep.outputs.version }}"' -i charts/captain-hook/Chart.yaml 99 | - name: Install Helm 100 | uses: azure/setup-helm@18bc76811624f360dbd7f18c2d4ecb32c7b87bab # v1 101 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 102 | with: 103 | version: v3.5.2 104 | - name: Run chart-releaser 105 | uses: helm/chart-releaser-action@120944e66390c2534cc1b3c62d7285ba7ff02594 # v1.2.0 106 | if: ${{ steps.prep.outputs.release_chart == 'true' }} 107 | env: 108 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 109 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | NAME := captain-hook 3 | BINARY_NAME := captain-hook 4 | GO := GO111MODULE=on GO15VENDOREXPERIMENT=1 go 5 | GO_NOMOD := GO111MODULE=off go 6 | PACKAGE_NAME := github.com/jenkins-infra/captain-hook 7 | ROOT_PACKAGE := github.com/jenkins-infra/captain-hook 8 | ORG := jenkins-infra 9 | 10 | # set dev version unless VERSION is explicitly set via environment 11 | VERSION ?= $(shell echo "$$(git describe --abbrev=0 --tags 2>/dev/null)-dev+$(REV)" | sed 's/^v//') 12 | 13 | GO_VERSION := $(shell $(GO) version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/') 14 | PACKAGE_DIRS := $(shell $(GO) list ./... | grep -v /vendor/ | grep -v e2e) 15 | PEGOMOCK_PACKAGE := github.com/petergtz/pegomock 16 | GO_DEPENDENCIES := $(shell find . -type f -name '*.go') 17 | 18 | REV := $(shell git rev-parse --short HEAD 2> /dev/null || echo 'unknown') 19 | SHA1 := $(shell git rev-parse HEAD 2> /dev/null || echo 'unknown') 20 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2> /dev/null || echo 'unknown') 21 | BUILD_DATE := $(shell date +%Y%m%d-%H:%M:%S) 22 | INSPECT_LABELS := $(shell inspect labels 2> /dev/null || echo '--label "inspect.not.installed=true"') 23 | 24 | BUILDFLAGS := -trimpath -ldflags \ 25 | " -X $(ROOT_PACKAGE)/pkg/version.Version=$(VERSION)\ 26 | -X $(ROOT_PACKAGE)/pkg/version.Revision=$(REV)\ 27 | -X $(ROOT_PACKAGE)/pkg/version.BuiltBy=make \ 28 | -X $(ROOT_PACKAGE)/pkg/version.Sha1=$(SHA1)\ 29 | -X $(ROOT_PACKAGE)/pkg/version.Branch='$(BRANCH)'\ 30 | -X $(ROOT_PACKAGE)/pkg/version.BuildDate='$(BUILD_DATE)'\ 31 | -X $(ROOT_PACKAGE)/pkg/version.GoVersion='$(GO_VERSION)'" 32 | CGO_ENABLED = 0 33 | BUILDTAGS := 34 | 35 | GOPATH1=$(firstword $(subst :, ,$(GOPATH))) 36 | 37 | export PATH := $(PATH):$(GOPATH1)/bin 38 | 39 | CLIENTSET_NAME_VERSIONED := v0.15.11 40 | 41 | build: $(GO_DEPENDENCIES) 42 | CGO_ENABLED=$(CGO_ENABLED) $(GO) build $(BUILDTAGS) $(BUILDFLAGS) -o build/$(BINARY_NAME) cmd/$(NAME)/$(NAME).go 43 | 44 | linux: $(GO_DEPENDENCIES) 45 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 $(GO) build $(BUILDFLAGS) -o build/linux/$(NAME) cmd/$(NAME)/$(NAME).go 46 | chmod +x build/linux/$(NAME) 47 | 48 | dist: $(GO_DEPENDENCIES) 49 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 $(GO) build $(BUILDFLAGS) -o dist/captain-hook-linux_linux_amd64/$(NAME) cmd/$(NAME)/$(NAME).go 50 | chmod +x dist/captain-hook-linux_linux_amd64/$(NAME) 51 | docker build --platform linux/amd64 -t jenkinsciinfra/captain-hook:dev $(INSPECT_LABELS) . 52 | docker push jenkinsciinfra/captain-hook:dev --disable-content-trust=true 53 | 54 | diff: 55 | helm diff upgrade --install captain-hook charts/captain-hook --set image.pullPolicy=Always --set replicaCount=1 56 | 57 | deploy: 58 | helm upgrade --install captain-hook charts/captain-hook --set image.pullPolicy=Always --set replicaCount=1 59 | 60 | arm: $(GO_DEPENDENCIES) 61 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=arm $(GO) build $(BUILDFLAGS) -o build/arm/$(NAME) cmd/$(NAME)/$(NAME).go 62 | chmod +x build/arm/$(NAME) 63 | 64 | win: $(GO_DEPENDENCIES) 65 | CGO_ENABLED=$(CGO_ENABLED) GOOS=windows GOARCH=amd64 $(GO) build $(BUILDFLAGS) -o build/win/$(NAME)-windows-amd64.exe cmd/$(NAME)/$(NAME).go 66 | 67 | darwin: $(GO_DEPENDENCIES) 68 | CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 $(GO) build $(BUILDFLAGS) -o build/darwin/$(NAME) cmd/$(NAME)/$(NAME).go 69 | chmod +x build/darwin/$(NAME) 70 | 71 | deploy-local: build 72 | mkdir -p ~/bin 73 | cp build/$(BINARY_NAME) ~/bin/$(BINARY_NAME) 74 | 75 | all: version check 76 | 77 | check: fmt lint build test 78 | 79 | version: 80 | echo "Go version: $(GO_VERSION)" 81 | 82 | test: 83 | DISABLE_SSO=true CGO_ENABLED=$(CGO_ENABLED) $(GO) test -coverprofile=coverage.out $(PACKAGE_DIRS) 84 | 85 | testv: 86 | DISABLE_SSO=true CGO_ENABLED=$(CGO_ENABLED) $(GO) test -test.v $(PACKAGE_DIRS) 87 | 88 | testrich: 89 | DISABLE_SSO=true CGO_ENABLED=$(CGO_ENABLED) richgo test -test.v $(PACKAGE_DIRS) 90 | 91 | test1: 92 | DISABLE_SSO=true CGO_ENABLED=$(CGO_ENABLED) $(GO) test -count=1 -short ./... -test.v -run $(TEST) 93 | 94 | cover: 95 | $(GO) tool cover -func coverage.out | grep total 96 | 97 | coverage: 98 | $(GO) tool cover -html=coverage.out 99 | 100 | install: $(GO_DEPENDENCIES) 101 | GOBIN=${GOPATH1}/bin $(GO) install $(BUILDFLAGS) cmd/$(NAME)/$(NAME).go 102 | 103 | get-fmt-deps: ## Install goimports 104 | $(GO_NOMOD) get golang.org/x/tools/cmd/goimports 105 | 106 | importfmt: get-fmt-deps 107 | @echo "Formatting the imports..." 108 | goimports -w $(GO_DEPENDENCIES) 109 | 110 | fmt: importfmt 111 | @FORMATTED=`$(GO) fmt $(PACKAGE_DIRS)` 112 | @([[ ! -z "$(FORMATTED)" ]] && printf "Fixed unformatted files:\n$(FORMATTED)") || true 113 | 114 | clean: 115 | rm -rf build release 116 | 117 | modtidy: 118 | $(GO) mod tidy 119 | 120 | mod: modtidy build 121 | 122 | .PHONY: dist release clean 123 | 124 | 125 | generate-fakes: 126 | $(GO) generate ./... 127 | 128 | generate-all: generate-fakes 129 | 130 | .PHONY: goreleaser 131 | goreleaser: 132 | step-go-releaser --organisation=$(ORG) --revision=$(REV) --branch=$(BRANCH) --build-date=$(BUILD_DATE) --go-version=$(GO_VERSION) --root-package=$(ROOT_PACKAGE) --version=$(VERSION) 133 | 134 | lint: 135 | golangci-lint run 136 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/captainhookio/v1alpha1/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by client-gen. DO NOT EDIT. 6 | 7 | package v1alpha1 8 | 9 | import ( 10 | "context" 11 | "time" 12 | 13 | v1alpha1 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 14 | scheme "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/scheme" 15 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | types "k8s.io/apimachinery/pkg/types" 17 | watch "k8s.io/apimachinery/pkg/watch" 18 | rest "k8s.io/client-go/rest" 19 | ) 20 | 21 | // HooksGetter has a method to return a HookInterface. 22 | // A group's client should implement this interface. 23 | type HooksGetter interface { 24 | Hooks(namespace string) HookInterface 25 | } 26 | 27 | // HookInterface has methods to work with Hook resources. 28 | type HookInterface interface { 29 | Create(ctx context.Context, hook *v1alpha1.Hook, opts v1.CreateOptions) (*v1alpha1.Hook, error) 30 | Update(ctx context.Context, hook *v1alpha1.Hook, opts v1.UpdateOptions) (*v1alpha1.Hook, error) 31 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 32 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 33 | Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Hook, error) 34 | List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.HookList, error) 35 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 36 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Hook, err error) 37 | HookExpansion 38 | } 39 | 40 | // hooks implements HookInterface 41 | type hooks struct { 42 | client rest.Interface 43 | ns string 44 | } 45 | 46 | // newHooks returns a Hooks 47 | func newHooks(c *CaptainhookV1alpha1Client, namespace string) *hooks { 48 | return &hooks{ 49 | client: c.RESTClient(), 50 | ns: namespace, 51 | } 52 | } 53 | 54 | // Get takes name of the hook, and returns the corresponding hook object, and an error if there is any. 55 | func (c *hooks) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Hook, err error) { 56 | result = &v1alpha1.Hook{} 57 | err = c.client.Get(). 58 | Namespace(c.ns). 59 | Resource("hooks"). 60 | Name(name). 61 | VersionedParams(&options, scheme.ParameterCodec). 62 | Do(ctx). 63 | Into(result) 64 | return 65 | } 66 | 67 | // List takes label and field selectors, and returns the list of Hooks that match those selectors. 68 | func (c *hooks) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.HookList, err error) { 69 | var timeout time.Duration 70 | if opts.TimeoutSeconds != nil { 71 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 72 | } 73 | result = &v1alpha1.HookList{} 74 | err = c.client.Get(). 75 | Namespace(c.ns). 76 | Resource("hooks"). 77 | VersionedParams(&opts, scheme.ParameterCodec). 78 | Timeout(timeout). 79 | Do(ctx). 80 | Into(result) 81 | return 82 | } 83 | 84 | // Watch returns a watch.Interface that watches the requested hooks. 85 | func (c *hooks) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 86 | var timeout time.Duration 87 | if opts.TimeoutSeconds != nil { 88 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 89 | } 90 | opts.Watch = true 91 | return c.client.Get(). 92 | Namespace(c.ns). 93 | Resource("hooks"). 94 | VersionedParams(&opts, scheme.ParameterCodec). 95 | Timeout(timeout). 96 | Watch(ctx) 97 | } 98 | 99 | // Create takes the representation of a hook and creates it. Returns the server's representation of the hook, and an error, if there is any. 100 | func (c *hooks) Create(ctx context.Context, hook *v1alpha1.Hook, opts v1.CreateOptions) (result *v1alpha1.Hook, err error) { 101 | result = &v1alpha1.Hook{} 102 | err = c.client.Post(). 103 | Namespace(c.ns). 104 | Resource("hooks"). 105 | VersionedParams(&opts, scheme.ParameterCodec). 106 | Body(hook). 107 | Do(ctx). 108 | Into(result) 109 | return 110 | } 111 | 112 | // Update takes the representation of a hook and updates it. Returns the server's representation of the hook, and an error, if there is any. 113 | func (c *hooks) Update(ctx context.Context, hook *v1alpha1.Hook, opts v1.UpdateOptions) (result *v1alpha1.Hook, err error) { 114 | result = &v1alpha1.Hook{} 115 | err = c.client.Put(). 116 | Namespace(c.ns). 117 | Resource("hooks"). 118 | Name(hook.Name). 119 | VersionedParams(&opts, scheme.ParameterCodec). 120 | Body(hook). 121 | Do(ctx). 122 | Into(result) 123 | return 124 | } 125 | 126 | // Delete takes name of the hook and deletes it. Returns an error if one occurs. 127 | func (c *hooks) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 128 | return c.client.Delete(). 129 | Namespace(c.ns). 130 | Resource("hooks"). 131 | Name(name). 132 | Body(&opts). 133 | Do(ctx). 134 | Error() 135 | } 136 | 137 | // DeleteCollection deletes a collection of objects. 138 | func (c *hooks) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 139 | var timeout time.Duration 140 | if listOpts.TimeoutSeconds != nil { 141 | timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second 142 | } 143 | return c.client.Delete(). 144 | Namespace(c.ns). 145 | Resource("hooks"). 146 | VersionedParams(&listOpts, scheme.ParameterCodec). 147 | Timeout(timeout). 148 | Body(&opts). 149 | Do(ctx). 150 | Error() 151 | } 152 | 153 | // Patch applies the patch and returns the patched hook. 154 | func (c *hooks) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Hook, err error) { 155 | result = &v1alpha1.Hook{} 156 | err = c.client.Patch(pt). 157 | Namespace(c.ns). 158 | Resource("hooks"). 159 | Name(name). 160 | SubResource(subresources...). 161 | VersionedParams(&opts, scheme.ParameterCodec). 162 | Body(data). 163 | Do(ctx). 164 | Into(result) 165 | return 166 | } 167 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Generated Code 3 | */ 4 | 5 | // Code generated by informer-gen. DO NOT EDIT. 6 | 7 | package externalversions 8 | 9 | import ( 10 | reflect "reflect" 11 | sync "sync" 12 | time "time" 13 | 14 | versioned "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned" 15 | captainhookio "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions/captainhookio" 16 | internalinterfaces "github.com/jenkins-infra/captain-hook/pkg/client/informers/externalversions/internalinterfaces" 17 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | runtime "k8s.io/apimachinery/pkg/runtime" 19 | schema "k8s.io/apimachinery/pkg/runtime/schema" 20 | cache "k8s.io/client-go/tools/cache" 21 | ) 22 | 23 | // SharedInformerOption defines the functional option type for SharedInformerFactory. 24 | type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory 25 | 26 | type sharedInformerFactory struct { 27 | client versioned.Interface 28 | namespace string 29 | tweakListOptions internalinterfaces.TweakListOptionsFunc 30 | lock sync.Mutex 31 | defaultResync time.Duration 32 | customResync map[reflect.Type]time.Duration 33 | 34 | informers map[reflect.Type]cache.SharedIndexInformer 35 | // startedInformers is used for tracking which informers have been started. 36 | // This allows Start() to be called multiple times safely. 37 | startedInformers map[reflect.Type]bool 38 | } 39 | 40 | // WithCustomResyncConfig sets a custom resync period for the specified informer types. 41 | func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { 42 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 43 | for k, v := range resyncConfig { 44 | factory.customResync[reflect.TypeOf(k)] = v 45 | } 46 | return factory 47 | } 48 | } 49 | 50 | // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. 51 | func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { 52 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 53 | factory.tweakListOptions = tweakListOptions 54 | return factory 55 | } 56 | } 57 | 58 | // WithNamespace limits the SharedInformerFactory to the specified namespace. 59 | func WithNamespace(namespace string) SharedInformerOption { 60 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 61 | factory.namespace = namespace 62 | return factory 63 | } 64 | } 65 | 66 | // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. 67 | func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { 68 | return NewSharedInformerFactoryWithOptions(client, defaultResync) 69 | } 70 | 71 | // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. 72 | // Listers obtained via this SharedInformerFactory will be subject to the same filters 73 | // as specified here. 74 | // Deprecated: Please use NewSharedInformerFactoryWithOptions instead 75 | func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { 76 | return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) 77 | } 78 | 79 | // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. 80 | func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { 81 | factory := &sharedInformerFactory{ 82 | client: client, 83 | namespace: v1.NamespaceAll, 84 | defaultResync: defaultResync, 85 | informers: make(map[reflect.Type]cache.SharedIndexInformer), 86 | startedInformers: make(map[reflect.Type]bool), 87 | customResync: make(map[reflect.Type]time.Duration), 88 | } 89 | 90 | // Apply all options 91 | for _, opt := range options { 92 | factory = opt(factory) 93 | } 94 | 95 | return factory 96 | } 97 | 98 | // Start initializes all requested informers. 99 | func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { 100 | f.lock.Lock() 101 | defer f.lock.Unlock() 102 | 103 | for informerType, informer := range f.informers { 104 | if !f.startedInformers[informerType] { 105 | go informer.Run(stopCh) 106 | f.startedInformers[informerType] = true 107 | } 108 | } 109 | } 110 | 111 | // WaitForCacheSync waits for all started informers' cache were synced. 112 | func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { 113 | informers := func() map[reflect.Type]cache.SharedIndexInformer { 114 | f.lock.Lock() 115 | defer f.lock.Unlock() 116 | 117 | informers := map[reflect.Type]cache.SharedIndexInformer{} 118 | for informerType, informer := range f.informers { 119 | if f.startedInformers[informerType] { 120 | informers[informerType] = informer 121 | } 122 | } 123 | return informers 124 | }() 125 | 126 | res := map[reflect.Type]bool{} 127 | for informType, informer := range informers { 128 | res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) 129 | } 130 | return res 131 | } 132 | 133 | // InternalInformerFor returns the SharedIndexInformer for obj using an internal 134 | // client. 135 | func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { 136 | f.lock.Lock() 137 | defer f.lock.Unlock() 138 | 139 | informerType := reflect.TypeOf(obj) 140 | informer, exists := f.informers[informerType] 141 | if exists { 142 | return informer 143 | } 144 | 145 | resyncPeriod, exists := f.customResync[informerType] 146 | if !exists { 147 | resyncPeriod = f.defaultResync 148 | } 149 | 150 | informer = newFunc(f.client, resyncPeriod) 151 | f.informers[informerType] = informer 152 | 153 | return informer 154 | } 155 | 156 | // SharedInformerFactory provides shared informers for resources in all known 157 | // API group versions. 158 | type SharedInformerFactory interface { 159 | internalinterfaces.SharedInformerFactory 160 | ForResource(resource schema.GroupVersionResource) (GenericInformer, error) 161 | WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool 162 | 163 | Captainhook() captainhookio.Interface 164 | } 165 | 166 | func (f *sharedInformerFactory) Captainhook() captainhookio.Interface { 167 | return captainhookio.New(f, f.namespace, f.tweakListOptions) 168 | } 169 | -------------------------------------------------------------------------------- /pkg/store/kubernetes_store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | v1alpha12 "github.com/jenkins-infra/captain-hook/pkg/api/captainhookio/v1alpha1" 8 | "github.com/jenkins-infra/captain-hook/pkg/client/clientset/versioned/fake" 9 | "github.com/stretchr/testify/assert" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | clienttesting "k8s.io/client-go/testing" 13 | ) 14 | 15 | func TestKubernetesStore_StoreHook(t *testing.T) { 16 | f := fake.Clientset{} 17 | 18 | store := kubernetesStore{ 19 | namespace: "dummy", 20 | client: &f, 21 | } 22 | 23 | // return a hook with a generated hook name as the fake 24 | f.AddReactor("create", "hooks", 25 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 26 | hook := action.(clienttesting.UpdateAction).GetObject().(*v1alpha12.Hook) 27 | assert.Equal(t, hook.Name, "") 28 | assert.Equal(t, hook.GenerateName, "hook-") 29 | 30 | assert.Equal(t, hook.Spec.ForwardURL, "http://thing.com") 31 | assert.Equal(t, hook.Spec.Body, "OK") 32 | assert.Equal(t, hook.Spec.Headers, make(map[string][]string)) 33 | 34 | assert.Equal(t, hook.Status.Phase, v1alpha12.HookPhasePending) 35 | 36 | hook.ObjectMeta.Name = "generatedHookName" 37 | return true, hook, nil 38 | }) 39 | 40 | hookName, err := store.StoreHook("http://thing.com", []byte("OK"), make(http.Header)) 41 | assert.NoError(t, err) 42 | assert.Equal(t, "generatedHookName", hookName) 43 | assert.Equal(t, 1, len(f.Actions())) 44 | assert.Equal(t, "create", f.Actions()[0].GetVerb()) 45 | assert.Equal(t, "hooks", f.Actions()[0].GetResource().Resource) 46 | assert.Equal(t, "v1alpha1", f.Actions()[0].GetResource().Version) 47 | assert.Equal(t, "captainhook.io", f.Actions()[0].GetResource().Group) 48 | } 49 | 50 | func TestKubernetesStore_Success(t *testing.T) { 51 | f := fake.Clientset{} 52 | 53 | store := kubernetesStore{ 54 | namespace: "dummy", 55 | client: &f, 56 | } 57 | 58 | f.AddReactor("get", "hooks", 59 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 60 | return true, &v1alpha12.Hook{ 61 | ObjectMeta: v1.ObjectMeta{ 62 | Name: "generatedHookName", 63 | }, 64 | Spec: v1alpha12.HookSpec{ 65 | ForwardURL: "http://test.com", 66 | Body: "body", 67 | Headers: nil, 68 | }, 69 | Status: v1alpha12.HookStatus{ 70 | Phase: v1alpha12.HookPhasePending, 71 | Attempts: 0, 72 | }, 73 | }, nil 74 | }) 75 | 76 | f.AddReactor("update", "hooks", 77 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 78 | hook := action.(clienttesting.UpdateAction).GetObject().(*v1alpha12.Hook) 79 | assert.Equal(t, hook.Name, "generatedHookName") 80 | 81 | assert.Equal(t, hook.Status.Phase, v1alpha12.HookPhaseSuccess) 82 | assert.Equal(t, hook.Status.Message, "") 83 | assert.NotNil(t, hook.Status.CompletedTimestamp) 84 | 85 | return true, hook, nil 86 | }) 87 | 88 | err := store.Success("hookName") 89 | assert.NoError(t, err) 90 | 91 | assert.Equal(t, 2, len(f.Actions())) 92 | 93 | assert.Equal(t, "get", f.Actions()[0].GetVerb()) 94 | assert.Equal(t, "hooks", f.Actions()[0].GetResource().Resource) 95 | assert.Equal(t, "v1alpha1", f.Actions()[0].GetResource().Version) 96 | assert.Equal(t, "captainhook.io", f.Actions()[0].GetResource().Group) 97 | 98 | assert.Equal(t, "update", f.Actions()[1].GetVerb()) 99 | assert.Equal(t, "hooks", f.Actions()[1].GetResource().Resource) 100 | assert.Equal(t, "v1alpha1", f.Actions()[1].GetResource().Version) 101 | assert.Equal(t, "captainhook.io", f.Actions()[1].GetResource().Group) 102 | } 103 | 104 | func TestKubernetesStore_Error(t *testing.T) { 105 | f := fake.Clientset{} 106 | 107 | store := kubernetesStore{ 108 | namespace: "dummy", 109 | client: &f, 110 | } 111 | 112 | f.AddReactor("get", "hooks", 113 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 114 | return true, &v1alpha12.Hook{ 115 | ObjectMeta: v1.ObjectMeta{ 116 | Name: "generatedHookName", 117 | }, 118 | Spec: v1alpha12.HookSpec{ 119 | ForwardURL: "http://test.com", 120 | Body: "body", 121 | Headers: nil, 122 | }, 123 | Status: v1alpha12.HookStatus{ 124 | Phase: v1alpha12.HookPhasePending, 125 | Attempts: 0, 126 | }, 127 | }, nil 128 | }) 129 | 130 | f.AddReactor("update", "hooks", 131 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 132 | hook := action.(clienttesting.UpdateAction).GetObject().(*v1alpha12.Hook) 133 | assert.Equal(t, hook.Name, "generatedHookName") 134 | 135 | assert.Equal(t, hook.Status.Phase, v1alpha12.HookPhaseFailed) 136 | assert.Equal(t, hook.Status.Message, "dummyMessage") 137 | 138 | return true, hook, nil 139 | }) 140 | 141 | err := store.Error("hookName", "dummyMessage") 142 | assert.NoError(t, err) 143 | 144 | assert.Equal(t, 2, len(f.Actions())) 145 | 146 | assert.Equal(t, "get", f.Actions()[0].GetVerb()) 147 | assert.Equal(t, "hooks", f.Actions()[0].GetResource().Resource) 148 | assert.Equal(t, "v1alpha1", f.Actions()[0].GetResource().Version) 149 | assert.Equal(t, "captainhook.io", f.Actions()[0].GetResource().Group) 150 | 151 | assert.Equal(t, "update", f.Actions()[1].GetVerb()) 152 | assert.Equal(t, "hooks", f.Actions()[1].GetResource().Resource) 153 | assert.Equal(t, "v1alpha1", f.Actions()[1].GetResource().Version) 154 | assert.Equal(t, "captainhook.io", f.Actions()[1].GetResource().Group) 155 | } 156 | 157 | func TestKubernetesStore_MarkForRetry(t *testing.T) { 158 | f := fake.Clientset{} 159 | 160 | store := kubernetesStore{ 161 | namespace: "dummy", 162 | client: &f, 163 | } 164 | 165 | f.AddReactor("get", "hooks", 166 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 167 | return true, &v1alpha12.Hook{ 168 | ObjectMeta: v1.ObjectMeta{ 169 | Name: "generatedHookName", 170 | }, 171 | Spec: v1alpha12.HookSpec{ 172 | ForwardURL: "http://test.com", 173 | Body: "body", 174 | Headers: nil, 175 | }, 176 | Status: v1alpha12.HookStatus{ 177 | Phase: v1alpha12.HookPhaseFailed, 178 | Attempts: 0, 179 | }, 180 | }, nil 181 | }) 182 | 183 | f.AddReactor("update", "hooks", 184 | func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { 185 | hook := action.(clienttesting.UpdateAction).GetObject().(*v1alpha12.Hook) 186 | assert.Equal(t, hook.Name, "generatedHookName") 187 | 188 | assert.Equal(t, hook.Status.Phase, v1alpha12.HookPhasePending) 189 | assert.Equal(t, hook.Status.Message, "") 190 | assert.Equal(t, hook.Status.Attempts, 1) 191 | 192 | return true, hook, nil 193 | }) 194 | 195 | err := store.MarkForRetry("hookName") 196 | assert.NoError(t, err) 197 | 198 | assert.Equal(t, 2, len(f.Actions())) 199 | 200 | assert.Equal(t, "get", f.Actions()[0].GetVerb()) 201 | assert.Equal(t, "hooks", f.Actions()[0].GetResource().Resource) 202 | assert.Equal(t, "v1alpha1", f.Actions()[0].GetResource().Version) 203 | assert.Equal(t, "captainhook.io", f.Actions()[0].GetResource().Group) 204 | 205 | assert.Equal(t, "update", f.Actions()[1].GetVerb()) 206 | assert.Equal(t, "hooks", f.Actions()[1].GetResource().Resource) 207 | assert.Equal(t, "v1alpha1", f.Actions()[1].GetResource().Version) 208 | assert.Equal(t, "captainhook.io", f.Actions()[1].GetResource().Group) 209 | } 210 | 211 | func TestKubernetesStore_Delete(t *testing.T) { 212 | f := fake.Clientset{} 213 | 214 | store := kubernetesStore{ 215 | namespace: "dummy", 216 | client: &f, 217 | } 218 | 219 | err := store.Delete("hookName") 220 | assert.NoError(t, err) 221 | 222 | assert.Equal(t, 1, len(f.Actions())) 223 | 224 | assert.Equal(t, "delete", f.Actions()[0].GetVerb()) 225 | assert.Equal(t, "hooks", f.Actions()[0].GetResource().Resource) 226 | assert.Equal(t, "v1alpha1", f.Actions()[0].GetResource().Version) 227 | assert.Equal(t, "captainhook.io", f.Actions()[0].GetResource().Group) 228 | } 229 | -------------------------------------------------------------------------------- /pkg/hook/testdata/push.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/master", 3 | "before": "a10867b14bb761a232cd80139fbd4c0d33264240", 4 | "after": "199eddf46df50de8d02e99bf1c5fdb4101338224", 5 | "created": false, 6 | "deleted": false, 7 | "forced": false, 8 | "base_ref": null, 9 | "compare": "https://github.com/Codertocat/Hello-World/compare/a10867b14bb7...000000000000", 10 | "commits": [ 11 | 12 | ], 13 | "head_commit": { 14 | "id": "199eddf46df50de8d02e99bf1c5fdb4101338224", 15 | "tree_id": "3bb5fd1cf9829a051ca3d4bd6839f0aec10a33fb", 16 | "distinct": true, 17 | "message": "Update README", 18 | "timestamp": "2018-06-15T13:01:51-07:00", 19 | "url": "https://github.com/Codertocat/Hello-World/compare/199eddf46df50de8d02e99bf1c5fdb4101338224", 20 | "author": { 21 | "name": "Codertocat", 22 | "email": "21031067+Codertocat@users.noreply.github.com", 23 | "username": "Codertocat" 24 | }, 25 | "committer": { 26 | "name": "GitHub", 27 | "email": "noreply@github.com", 28 | "username": "web-flow" 29 | }, 30 | "added": [ 31 | 32 | ], 33 | "removed": [ 34 | 35 | ], 36 | "modified": [ 37 | "README.md" 38 | ] 39 | }, 40 | "repository": { 41 | "id": 135493233, 42 | "node_id": "MDEwOlJlcG9zaXRvcnkxMzU0OTMyMzM=", 43 | "name": "Hello-World", 44 | "full_name": "Codertocat/Hello-World", 45 | "owner": { 46 | "name": "Codertocat", 47 | "email": "21031067+Codertocat@users.noreply.github.com", 48 | "login": "Codertocat", 49 | "id": 21031067, 50 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 51 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 52 | "gravatar_id": "", 53 | "url": "https://api.github.com/users/Codertocat", 54 | "html_url": "https://github.com/Codertocat", 55 | "followers_url": "https://api.github.com/users/Codertocat/followers", 56 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 57 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 58 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 59 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 60 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 61 | "repos_url": "https://api.github.com/users/Codertocat/repos", 62 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 63 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 64 | "type": "User", 65 | "site_admin": false 66 | }, 67 | "private": false, 68 | "html_url": "https://github.com/Codertocat/Hello-World", 69 | "description": null, 70 | "fork": false, 71 | "url": "https://github.com/Codertocat/Hello-World", 72 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 73 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 74 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 75 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 76 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 77 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 78 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 79 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 80 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 81 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 82 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 83 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 84 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 85 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 86 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 87 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 88 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 89 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 90 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 91 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 92 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 93 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 94 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 95 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 96 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 97 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 98 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 99 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 100 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 101 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 102 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 103 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 104 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 105 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 106 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 107 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 108 | "created_at": 1527711484, 109 | "updated_at": "2018-05-30T20:18:35Z", 110 | "pushed_at": 1527711528, 111 | "git_url": "git://github.com/Codertocat/Hello-World.git", 112 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 113 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 114 | "svn_url": "https://github.com/Codertocat/Hello-World", 115 | "homepage": null, 116 | "size": 0, 117 | "stargazers_count": 0, 118 | "watchers_count": 0, 119 | "language": null, 120 | "has_issues": true, 121 | "has_projects": true, 122 | "has_downloads": true, 123 | "has_wiki": true, 124 | "has_pages": true, 125 | "forks_count": 0, 126 | "mirror_url": null, 127 | "archived": false, 128 | "open_issues_count": 2, 129 | "license": null, 130 | "forks": 0, 131 | "open_issues": 2, 132 | "watchers": 0, 133 | "default_branch": "master", 134 | "stargazers": 0, 135 | "master_branch": "master" 136 | }, 137 | "pusher": { 138 | "name": "Codertocat", 139 | "email": "21031067+Codertocat@users.noreply.github.com" 140 | }, 141 | "sender": { 142 | "login": "Codertocat", 143 | "id": 21031067, 144 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 145 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 146 | "gravatar_id": "", 147 | "url": "https://api.github.com/users/Codertocat", 148 | "html_url": "https://github.com/Codertocat", 149 | "followers_url": "https://api.github.com/users/Codertocat/followers", 150 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 151 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 152 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 153 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 154 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 155 | "repos_url": "https://api.github.com/users/Codertocat/repos", 156 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 157 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 158 | "type": "User", 159 | "site_admin": false 160 | }, 161 | "installation": {"id":7486037,"node_id":"MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzQ4NjAzNw=="} 162 | } -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | --------------------------------------------------------------------------------