From 9112b0a2e3cb2b89caed8560858178f125296b15 Mon Sep 17 00:00:00 2001 From: Tim Collins Date: Mon, 15 Jul 2024 12:32:02 +0100 Subject: [PATCH 1/5] feat: option to run github-notifier as a daemon that you call Signed-off-by: Tim Collins --- ci-github-notifier/main.go | 113 ++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 27 deletions(-) diff --git a/ci-github-notifier/main.go b/ci-github-notifier/main.go index 6c809e3..e1b4cd0 100644 --- a/ci-github-notifier/main.go +++ b/ci-github-notifier/main.go @@ -2,44 +2,103 @@ package main import ( "encoding/json" - "fmt" "github.com/imroc/req" - "io/ioutil" - "log" - "os" + "flag" + "fmt" + "log" + "net/http" + "os" + "strings" ) func main() { - fmt.Printf("Notifiying Github: %s:%s\n", getValidatedEnvVar("context"), getValidatedEnvVar("state")) + var webServerModeFlag = flag.Bool("webServerMode", false, "Run in web server (daemon service) mode") + flag.Parse() - header := req.Header{ - "Authorization": fmt.Sprintf("token %s", getToken(os.Getenv("tokenFile"), "access_token")), - } + // Flag can be overridden by environment variable + webServerModeEnv := strings.ToLower(os.Getenv("WEBSERVER_MODE")) == "true" - values := map[string]string{"state": getValidatedEnvVar("state"), "target_url": getValidatedEnvVar("target_url"), "description": getValidatedEnvVar("description"), "context": getValidatedEnvVar("context")} - jsonData, err := json.Marshal(values) + // Determine web server mode by combining the flag and the environment variable + webServerMode := *webServerModeFlag || webServerModeEnv - r, err := req.Post(fmt.Sprintf("https://%s/repos/%s/%s/statuses/%s", getUrl("gh_url", "api.github.com"), getValidatedEnvVar("organisation"), getValidatedEnvVar("app_repo"), getValidatedEnvVar("git_sha")), header, req.BodyJSON(jsonData)) + if webServerMode { + fmt.Println("Running in web server mode") + startWebServer() + } else { + fmt.Println("Running in standalone mode") + notifyGitHub(nil, nil) + } +} - if err != nil { - log.Fatal(err) - } - resp := r.Response() - fmt.Println("HTTP response from github:", resp.StatusCode) +func startWebServer() { + http.HandleFunc("/notify", func(w http.ResponseWriter, r *http.Request) { + notifyGitHub(w, r) + }) + fmt.Println("Server is listening on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func notifyGitHub(w http.ResponseWriter, r *http.Request) { + // Helper function to get parameter value: first tries URL query, then falls back to environment variable. + getParamValue := func(paramName, envVarName string) string { + if r != nil { // Check if http.Request is available + values := r.URL.Query() + if value, found := values[paramName]; found && len(value) > 0 { + return value[0] + } + } + return getValidatedEnvVar(envVarName) + } + + // Parameters + state := getParamValue("state", "state") + targetURL := getParamValue("target_url", "target_url") + description := getParamValue("description", "description") + context := getParamValue("context", "context") + organisation := getParamValue("organisation", "organisation") + appRepo := getParamValue("app_repo", "app_repo") + gitSha := getParamValue("git_sha", "git_sha") + ghURL := getParamValue("gh_url", "gh_url") + + // Notification logic + fmt.Printf("Notifying Github: %s:%s\n", context, state) + + header := req.Header{ + "Authorization": fmt.Sprintf("token %s", getToken(os.Getenv("tokenFile"), "access_token")), + } + + values := map[string]string{"state": state, "target_url": targetURL, "description": description, "context": context} + jsonData, err := json.Marshal(values) + + resp, err := req.Post(fmt.Sprintf("https://%s/repos/%s/%s/statuses/%s", getUrl(ghURL, "api.github.com"), organisation, appRepo, gitSha), header, req.BodyJSON(jsonData)) + if err != nil { + if w != nil { // Check if http.ResponseWriter is available + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + fmt.Println("Error notifying GitHub:", err) + } + return + } + + httpResponse := resp.Response() + if w != nil { + fmt.Fprintf(w, "HTTP response from github: %d", httpResponse.StatusCode) + } else { + fmt.Printf("HTTP response from github: %d\n", httpResponse.StatusCode) + } } func getValidatedEnvVar(e string) string { - c := os.Getenv(e) - if os.Getenv(e) == "" { - fmt.Printf("Error: No environment variable called %s available. Exiting.\n", e) - os.Exit(1) - } - return c + c := os.Getenv(e) + if os.Getenv(e) == "" { + log.Fatalf("Error: No environment variable called %s available. Exiting.\n", e) + } + return c } func getToken(f string, e string) string { if os.Getenv(e) == "" { - data, err := ioutil.ReadFile(f) + data, err := os.ReadFile(f) if err != nil { fmt.Println("No tokenFile found. Falling back to Environment Variable") } @@ -50,8 +109,8 @@ func getToken(f string, e string) string { } func getUrl(e, fallback string) string { - if value, ok := os.LookupEnv(e); ok { - return value - } - return fallback + if value, ok := os.LookupEnv(e); ok { + return value + } + return fallback } From 139fad1ee989a204bf9e79ab1f629aa2b3426781 Mon Sep 17 00:00:00 2001 From: Tim Collins Date: Mon, 15 Jul 2024 12:33:51 +0100 Subject: [PATCH 2/5] alphabets are hard Signed-off-by: Tim Collins --- ci-github-notifier/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci-github-notifier/main.go b/ci-github-notifier/main.go index e1b4cd0..c62529c 100644 --- a/ci-github-notifier/main.go +++ b/ci-github-notifier/main.go @@ -3,12 +3,12 @@ package main import ( "encoding/json" "github.com/imroc/req" + "strings" "flag" "fmt" "log" "net/http" "os" - "strings" ) func main() { From 5f6b19fc0f0fa7632f56ff65d2fd959359538bce Mon Sep 17 00:00:00 2001 From: Tim Collins Date: Mon, 15 Jul 2024 12:50:16 +0100 Subject: [PATCH 3/5] example deployment Signed-off-by: Tim Collins --- Dockerfile | 1 + examples/daemon-deployment/README.md | 8 +++ examples/daemon-deployment/deployment.yaml | 76 ++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 examples/daemon-deployment/README.md create mode 100644 examples/daemon-deployment/deployment.yaml diff --git a/Dockerfile b/Dockerfile index f64cdb1..51fc0ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,5 @@ FROM scratch COPY --from=builder /go/bin/ci-github-notifier /go/bin/ci-github-notifier COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +EXPOSE 8080 ENTRYPOINT ["/go/bin/ci-github-notifier"] diff --git a/examples/daemon-deployment/README.md b/examples/daemon-deployment/README.md new file mode 100644 index 0000000..901d6c9 --- /dev/null +++ b/examples/daemon-deployment/README.md @@ -0,0 +1,8 @@ +# Web server mode (daemon) deployment + +This example demonstrates how to deploy the CI GitHub Notifier in web server mode (daemon) in Kubernetes. You deploy the CI GitHub Notifier as a Kubernetes Deployment and expose it as a Kubernetes Service. Then you can call it from other services in the same Kubernetes cluster (eg a running Argo Workflow). + +Call it with: +```bash +curl http://localhost:8080/notify?state=success&target_url=http%3A%2F%2Fexample.com&description=Deployment+successful&context=deployment&tokenFile=/path/to/tokenfile&organisation=myorg&app_repo=myrepo&git_sha=abc123&gh_url=api.github.com +``` diff --git a/examples/daemon-deployment/deployment.yaml b/examples/daemon-deployment/deployment.yaml new file mode 100644 index 0000000..80b0eba --- /dev/null +++ b/examples/daemon-deployment/deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: ci-github-notifier + name: ci-github-notifier +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + selector: + matchLabels: + app: ci-github-notifier + template: + metadata: + labels: + app: ci-github-notifier + spec: + containers: + - image: ghcr.io/crumbhole/ci-github-notifier + imagePullPolicy: Always + name: ci-github-notifier + ports: + - containerPort: 8080 + name: http + protocol: TCP + resources: + requests: + memory: 20Mi + cpu: 10m + env: + - name: WEBSERVER_MODE + value: "true" + - name: access_token + valueFrom: + secretKeyRef: + name: ci-github-notifier-access-token + key: access_token + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "app" + operator: In + values: + - ci-github-notifier + topologyKey: "topology.kubernetes.io/zone" +--- +apiVersion: v1 +kind: Secret +metadata: + name: ci-github-notifier-access-token +type: Opaque +data: + access_token: +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: ci-github-notifier + name: ci-github-notifier +spec: + ports: + - name: ci-github-notifier + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app: ci-github-notifier + type: ClusterIP From 07d4a469bb1763eac3014035a4f58ccc6fd90133 Mon Sep 17 00:00:00 2001 From: Tim Collins Date: Mon, 15 Jul 2024 13:04:06 +0100 Subject: [PATCH 4/5] more permissions Signed-off-by: Tim Collins --- examples/daemon-deployment/argo-workflow.yaml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 examples/daemon-deployment/argo-workflow.yaml diff --git a/examples/daemon-deployment/argo-workflow.yaml b/examples/daemon-deployment/argo-workflow.yaml new file mode 100644 index 0000000..60c78f1 --- /dev/null +++ b/examples/daemon-deployment/argo-workflow.yaml @@ -0,0 +1,29 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: ci-github-notifier-example- + namespace: ci +spec: + entrypoint: main + serviceAccountName: ci + templates: + - name: main + dag: + tasks: + - name: post-pending + template: ci-github-notifier + arguments: + parameters: + - name: base_url + value: "http://ci-github-notifier:8080/notify" + - name: state + value: pending + + - name: ci-github-notifier + inputs: + parameters: + - name: base_url + - name: state + http: + url: "{{inputs.parameters.base_url}}?state={{inputs.parameters.state}}" + successCondition: "response.body contains \"200\"" From d6ca7a4681df28e64e0dcbb69e80bdf4f7f1760e Mon Sep 17 00:00:00 2001 From: Tim Collins Date: Mon, 15 Jul 2024 13:23:28 +0100 Subject: [PATCH 5/5] Basic examples Signed-off-by: Tim Collins --- README.md | 3 +++ examples/daemon-deployment/argo-workflow.yaml | 21 ++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b242fb5..2681ad4 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,6 @@ docker run \ # Argo Workflows example A simple Argo Workflows template can be found in the examples directory. + +# Run as a daemon +You may wish to run as a permanent service in your Kubernetes cluster and then call the service using curl or similar to send your message. An example of how to do this can be found in the examples directory. diff --git a/examples/daemon-deployment/argo-workflow.yaml b/examples/daemon-deployment/argo-workflow.yaml index 60c78f1..0f4e363 100644 --- a/examples/daemon-deployment/argo-workflow.yaml +++ b/examples/daemon-deployment/argo-workflow.yaml @@ -2,10 +2,9 @@ apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: generateName: ci-github-notifier-example- - namespace: ci spec: entrypoint: main - serviceAccountName: ci + serviceAccountName: valid-sa-name-here #https://argo-workflows.readthedocs.io/en/latest/http-template/#argo-agent templates: - name: main dag: @@ -15,15 +14,27 @@ spec: arguments: parameters: - name: base_url - value: "http://ci-github-notifier:8080/notify" + value: "http://ci-github-notifier.ci-github-notifier.svc.cluster.local:8080/notify" + - name: git_sha + value: "12345" - name: state - value: pending + value: "pending" + - name: context + value: "UI Checks" + - name: description + value: "In progress — This check has started..." + - name: target_url + value: "https://pipekit.io/" - name: ci-github-notifier inputs: parameters: - name: base_url + - name: git_sha - name: state + - name: context + - name: description + - name: target_url http: - url: "{{inputs.parameters.base_url}}?state={{inputs.parameters.state}}" + url: "{{inputs.parameters.base_url}}?app_repo=foo&organisation=bar&git_sha={{inputs.parameters.git_sha}}&state={{inputs.parameters.state}}&context={{inputs.parameters.context}}&description={{inputs.parameters.description}}&target_url={{inputs.parameters.target_url}}" successCondition: "response.body contains \"200\""