From 84c6e3e1bbbf4ef3e59b9b02ce90429f139525b3 Mon Sep 17 00:00:00 2001 From: Catalin Date: Tue, 24 Sep 2024 15:15:30 +0300 Subject: [PATCH] added builder gh action, added slack-collector and slack-notify applications --- .github/workflows/builder.yaml | 56 ++++ Dockerfile | 1 + example/composition.yaml | 16 +- example/function-runtime.yaml | 3 - example/functions.yaml | 4 +- {package => example}/rbac.yaml | 9 +- example/xr.yaml | 12 +- fn.go | 418 +++++++++++++++----------- input/v1beta1/input.go | 8 +- internal/slack-collector/Dockerfile | 8 +- internal/slack-collector/main.go | 39 ++- internal/slack-notify/Dockerfile | 12 + internal/slack-notify/go.mod | 45 +++ internal/slack-notify/go.sum | 175 +++++++++++ internal/slack-notify/main.go | 161 ++++++++++ internal/slackchannel/slackchannel.go | 78 ++--- package/composition.yaml | 17 -- package/crossplane.yaml | 2 + package/poll.yaml | 17 +- 19 files changed, 782 insertions(+), 299 deletions(-) create mode 100644 .github/workflows/builder.yaml rename {package => example}/rbac.yaml (83%) create mode 100644 internal/slack-notify/Dockerfile create mode 100644 internal/slack-notify/go.mod create mode 100644 internal/slack-notify/go.sum create mode 100644 internal/slack-notify/main.go delete mode 100644 package/composition.yaml diff --git a/.github/workflows/builder.yaml b/.github/workflows/builder.yaml new file mode 100644 index 0000000..ee30a68 --- /dev/null +++ b/.github/workflows/builder.yaml @@ -0,0 +1,56 @@ +name: Builder +on: + push: + branches: + - main + +defaults: + run: + shell: bash + +env: + REGISTRY: ghcr.io + +jobs: + build-and-pack: + name: Build and pack + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: CR authentication + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@github.com" + + - name: Get Short GitHub SHA and Save in Environment Variable + run: echo "SHORT_GITHUB_SHA=$(git rev-parse --short=5 HEAD)" >> $GITHUB_ENV + + - name: Setup the Crossplane CLI + run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh" + + - name: Build and push slack-collector + run: docker build -f internal/slack-collector/Dockerfile . -t ${REGISTRY}/${{ github.repository }}/slack-collector:${SHORT_GITHUB_SHA} && docker push ${REGISTRY}/${{ github.repository }}/slack-collector:${SHORT_GITHUB_SHA} + + - name: Build and push slack-notify + run: docker build -f internal/slack-notify/Dockerfile . -t ${REGISTRY}/${{ github.repository }}/slack-notify:${SHORT_GITHUB_SHA} && docker push ${REGISTRY}/${{ github.repository }}/slack-notify:${SHORT_GITHUB_SHA} + + - name: Build function image + run: docker build . -t ${REGISTRY}/${{ github.repository }}:${SHORT_GITHUB_SHA} + + - name: Build ans push Package + run: ./crossplane xpkg build --package-file=function-poll.xpkg --package-root=package/ --embed-runtime-image=${REGISTRY}/${{ github.repository }}:${SHORT_GITHUB_SHA} && ./crossplane xpkg push -f function-poll.xpkg ${REGISTRY}/${{ github.repository }}:${SHORT_GITHUB_SHA} + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 64e579e..900dc99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,7 @@ RUN --mount=target=. \ # Produce the Function image. We use a very lightweight 'distroless' image that # does not include any of the build tools used in previous stages. FROM gcr.io/distroless/base-debian11 AS image +LABEL org.opencontainers.image.source=https://github.com/kndpio/function-poll WORKDIR / COPY --from=build /function /function EXPOSE 9443 diff --git a/example/composition.yaml b/example/composition.yaml index c920c71..67b828e 100644 --- a/example/composition.yaml +++ b/example/composition.yaml @@ -1,17 +1,21 @@ apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: - name: function-template-go + name: poll-composition spec: compositeTypeRef: - apiVersion: example.crossplane.io/v1 - kind: XR + apiVersion: kndp.io/v1alpha1 + kind: Poll mode: Pipeline pipeline: - step: run-the-template functionRef: name: function-template-go input: - providerConfigRef: "default" - deploymentName: "collect" - deploymentImage: "ghcr.io/kndpio/kndp/slack-collector:0.1.0" \ No newline at end of file + apiVersion: kndp.io/v1alpha1 + kind: Poll + providerConfigRef: "kndp-kubernetes-provider-config" + deploymentImage: "ghcr.io/kndpio/function-poll/slack-collector:d7a4b" + cronJobImage: "ghcr.io/kndpio/function-poll/slack-notify:d7a4b" + deploymentName: "slack-collector" + serviceAccountName: "slack-collector" \ No newline at end of file diff --git a/example/function-runtime.yaml b/example/function-runtime.yaml index 96cb806..63769ee 100644 --- a/example/function-runtime.yaml +++ b/example/function-runtime.yaml @@ -3,9 +3,6 @@ kind: DeploymentRuntimeConfig metadata: name: poll-function-runtime spec: - serviceAccountTemplate: - metadata: - name: poll-sa deploymentTemplate: spec: selector: diff --git a/example/functions.yaml b/example/functions.yaml index 7174c3b..e9d0831 100644 --- a/example/functions.yaml +++ b/example/functions.yaml @@ -5,9 +5,9 @@ metadata: name: function-template-go annotations: # This tells crossplane beta render to connect to the function locally. - render.crossplane.io/runtime: Development + # render.crossplane.io/runtime: Development spec: # This is ignored when using the Development runtime. - package: function-template-go + package: ghcr.io/kndpio/function-poll:d7a4b runtimeConfigRef: name: poll-function-runtime \ No newline at end of file diff --git a/package/rbac.yaml b/example/rbac.yaml similarity index 83% rename from package/rbac.yaml rename to example/rbac.yaml index 0033a27..d61162a 100644 --- a/package/rbac.yaml +++ b/example/rbac.yaml @@ -13,7 +13,10 @@ rules: resources: ["ingresses"] verbs: ["*"] - apiGroups: ["kndp.io"] - resources: ["meals"] + resources: ["polls"] + verbs: ["*"] +- apiGroups: ["kndp.io"] + resources: ["polls/status"] verbs: ["*"] - apiGroups: ["v1"] resources: ["services"] @@ -24,7 +27,7 @@ rules: apiVersion: v1 kind: ServiceAccount metadata: - name: poll-sa + name: slack-collector --- @@ -34,7 +37,7 @@ metadata: name: poll-cluster-role-binding subjects: - kind: ServiceAccount - name: poll-sa + name: slack-collector namespace: default roleRef: kind: ClusterRole diff --git a/example/xr.yaml b/example/xr.yaml index fc3927f..123fcd9 100644 --- a/example/xr.yaml +++ b/example/xr.yaml @@ -1,14 +1,14 @@ apiVersion: kndp.io/v1alpha1 kind: Poll metadata: - name: poll + name: meal spec: - deliveryTime: "" - dueOrderTime: "3000" - dueTakeTime: "" + deliveryTime: 0 + dueOrderTime: 15 + dueTakeTime: 0 voters: [] - status: "" - title: "" + title: "meal" + schedule: "0 * * * *" messages: question: "how are you?" response: "thank you for response." diff --git a/fn.go b/fn.go index 6530c8c..59a7ca5 100644 --- a/fn.go +++ b/fn.go @@ -2,15 +2,12 @@ package main import ( "context" - "encoding/json" "os" - "strconv" "time" "github.com/slack-go/slack" - corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/function-sdk-go/logging" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/request" @@ -28,163 +25,26 @@ type Function struct { } var ( - token = os.Getenv("SLACK_API_TOKEN") - channelID = os.Getenv("SLACK_CHANEL_ID") - secretName = os.Getenv("SECRET_NAME") + token = os.Getenv("SLACK_API_TOKEN") + channelID = os.Getenv("SLACK_CHANEL_ID") + secretName = os.Getenv("SECRET_NAME") + ngrokDomainName = os.Getenv("NGROK_DOMAIN_NAME") ) -// Transform K8s resources into unstructured -// It must create all resources that are needed for the deployment to work like rbac, etc -func (f *Function) transformK8sResource(input *v1beta1.Input, logger logging.Logger) composed.Unstructured { - deploymentTemplate := map[string]interface{}{ - "apiVersion": "kubernetes.crossplane.io/v1alpha1", - "kind": "Object", - "metadata": map[string]interface{}{ - "name": input.DeploymentName, - }, - "spec": map[string]interface{}{ - "forProvider": map[string]interface{}{ - "manifest": map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": input.DeploymentName, - "namespace": "default", - }, - "spec": map[string]interface{}{ - "replicas": 1, - "selector": map[string]interface{}{ - "matchLabels": map[string]interface{}{ - "app": "poll", - }, - }, - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "poll", - }, - }, - "spec": map[string]interface{}{ - "serviceAccountName": input.DeploymentName, - "containers": []map[string]interface{}{ - { - "name": "poll-container", - "image": input.DeploymentImage, - "envFrom": []map[string]interface{}{ - {"secretRef": map[string]interface{}{"name": secretName}}, - }, - "ports": []map[string]interface{}{ - { - "containerPort": 80, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "managementPolicy": "Default", - "providerConfigRef": map[string]interface{}{"name": input.ProviderConfigRef}, - }, - } - - var clusterRole = map[string]interface{}{ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "ClusterRole", - "metadata": map[string]interface{}{ - "name": "poll-cluster-role", - }, - "rules": []interface{}{ - map[string]interface{}{ - "apiGroups": []interface{}{"apps"}, - "resources": []interface{}{"deployments"}, - "verbs": []interface{}{"*"}, - }, - map[string]interface{}{ - "apiGroups": []interface{}{"batch"}, - "resources": []interface{}{"cronjobs"}, - "verbs": []interface{}{"*"}, - }, - map[string]interface{}{ - "apiGroups": []interface{}{"networking.k8s.io"}, - "resources": []interface{}{"ingresses"}, - "verbs": []interface{}{"*"}, - }, - map[string]interface{}{ - "apiGroups": []interface{}{"kndp.io"}, - "resources": []interface{}{"polls"}, - "verbs": []interface{}{"*"}, - }, - map[string]interface{}{ - "apiGroups": []interface{}{"v1"}, - "resources": []interface{}{"services"}, - "verbs": []interface{}{"*"}, - }, - }, - } - - var serviceAccount = map[string]interface{}{ - "apiVersion": "v1", - "kind": "ServiceAccount", - "metadata": map[string]interface{}{ - "name": input.DeploymentName, - }, - } - - var clusterRoleBinding = map[string]interface{}{ - "apiVersion": "rbac.authorization.k8s.io/v1", - "kind": "ClusterRoleBinding", - "metadata": map[string]interface{}{ - "name": "poll-cluster-role-binding", - }, - "subjects": []interface{}{ - map[string]interface{}{ - "kind": "ServiceAccount", - "name": input.DeploymentName, - "namespace": "default", - }, - }, - "roleRef": map[string]interface{}{ - "kind": "ClusterRole", - "name": "poll-cluster-role", - "apiGroup": "rbac.authorization.k8s.io", - }, - } - - unstructuredData := composed.Unstructured{} - for _, resource := range []map[string]interface{}{clusterRole, serviceAccount, clusterRoleBinding, deploymentTemplate} { - unstructuredDataByte, err := json.Marshal(resource) - if err != nil { - logger.Info("error marshalling resource", "warning", err) - } - err = json.Unmarshal(unstructuredDataByte, &unstructuredData) - if err != nil { - logger.Info("error unmarshalling resource", "warning", err) - } - } - - return unstructuredData -} - // Check if the dueOrderTime is passed or all users have voted -func checkDueOrderTimeAndVoteCount(xr *resource.Composite, currentTimestamp int, users []string, logger logging.Logger) bool { - dueOrderTimeString, _ := xr.Resource.GetString("spec.dueOrderTime") - dueOrderTime, _ := strconv.Atoi(dueOrderTimeString) +func checkDueOrderTimeAndVoteCount(xr *resource.Composite, currentTimestamp int, users []string) bool { + dueOrderTime, _ := xr.Resource.GetInteger("spec.dueOrderTime") + lastNotificationTime, _ := xr.Resource.GetInteger("status.lastNotificationTime") voters, _ := xr.Resource.GetStringArray("spec.voters") if users == nil { users = []string{""} } - - creationTimestamp, _ := xr.Resource.GetString("metadata.creationTimestamp") - parsedTime, _ := time.Parse(time.RFC3339, creationTimestamp) - timestampDue := int(parsedTime.Unix()) + dueOrderTime + if lastNotificationTime == 0 { + return false + } + timestampDue := int(lastNotificationTime) + int(dueOrderTime) return currentTimestamp >= timestampDue || len(voters) == len(users) -} -func setSyncedCondition(xr *resource.Composite, conditionStatus corev1.ConditionStatus) { - xr.Resource.SetConditions(v1.Condition{Type: v1.TypeSynced, Status: conditionStatus}) } // RunFunction adds a Deployment and the new object template to the desired state. @@ -205,34 +65,246 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ if err != nil { f.log.Info("cannot get conversation members", "warning", err) } - if checkDueOrderTimeAndVoteCount(xr, currentTimestamp, users, f.log) { - setSyncedCondition(xr, corev1.ConditionTrue) - err = response.SetDesiredCompositeResource(rsp, xr) - if err != nil { - return rsp, err + pollTitle, _ := xr.Resource.GetString("spec.title") + pollName, _ := xr.Resource.GetString("metadata.name") + schedule, _ := xr.Resource.GetString("spec.schedule") + question, _ := xr.Resource.GetString("spec.messages.question") + d, _ := xr.Resource.GetBool("status.done") + resultText, _ := xr.Resource.GetString("spec.messages.result") + + if checkDueOrderTimeAndVoteCount(xr, currentTimestamp, users) { + if !d { + xr.Resource.SetBool("status.done", true) + slackchannel.SlackOrder(input, api, xr, f.log, resultText) + } + xr.Resource.SetManagedFields(nil) + response.SetDesiredCompositeResource(rsp, xr) + + } else { + xr.Resource.SetManagedFields(nil) + response.SetDesiredCompositeResource(rsp, xr) + + deployment := composed.Unstructured{ + Unstructured: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kubernetes.crossplane.io/v1alpha2", + "kind": "Object", + "metadata": map[string]interface{}{ + "name": input.DeploymentName, + }, + "spec": map[string]interface{}{ + "forProvider": map[string]interface{}{ + "manifest": map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": input.DeploymentName, + "namespace": "default", + }, + "spec": map[string]interface{}{ + "replicas": 1, + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "app": "poll", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "poll", + }, + }, + "spec": map[string]interface{}{ + "serviceAccountName": input.ServiceAccountName, + "containers": []interface{}{ + map[string]interface{}{ + "name": "poll-container", + "image": input.DeploymentImage, + "envFrom": []interface{}{ + map[string]interface{}{ + "secretRef": map[string]interface{}{ + "name": secretName, + }, + }, + }, + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": 3000, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "providerConfigRef": map[string]interface{}{"name": input.ProviderConfigRef}, + }, + }, + }, } - status, _ := xr.Resource.GetString("spec.status") - if status != "done" { - slackchannel.SlackOrder(input, api, xr, f.log) + + svc := composed.Unstructured{ + Unstructured: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kubernetes.crossplane.io/v1alpha2", + "kind": "Object", + "metadata": map[string]interface{}{ + "name": "service-collector", + }, + "spec": map[string]interface{}{ + "forProvider": map[string]interface{}{ + "manifest": map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "service-collector", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "http", + "port": 80, + "targetPort": 3000, + }, + }, + "selector": map[string]interface{}{ + "app": "poll", + }, + }, + }, + }, + "providerConfigRef": map[string]interface{}{"name": input.ProviderConfigRef}, + }, + }, + }, } - err := xr.Resource.SetString("spec.status", "done") - if err != nil { - return rsp, err + + ingress := composed.Unstructured{ + Unstructured: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kubernetes.crossplane.io/v1alpha2", + "kind": "Object", + "metadata": map[string]interface{}{ + "name": "ingress-collector", + }, + "spec": map[string]interface{}{ + "forProvider": map[string]interface{}{ + "manifest": map[string]interface{}{ + "apiVersion": "networking.k8s.io/v1", + "kind": "Ingress", + "metadata": map[string]interface{}{ + "name": "collector", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "ingressClassName": "ngrok", + "rules": []interface{}{ + map[string]interface{}{ + "host": ngrokDomainName, + "http": map[string]interface{}{ + "paths": []interface{}{ + map[string]interface{}{ + "path": "/events", + "pathType": "Prefix", + "backend": map[string]interface{}{ + "service": map[string]interface{}{ + "name": "service-collector", + "port": map[string]interface{}{ + "number": 80, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "providerConfigRef": map[string]interface{}{"name": input.ProviderConfigRef}, + }, + }, + }, } - } else { - setSyncedCondition(xr, corev1.ConditionFalse) - q, _ := xr.Resource.GetString("spec.messages.question") - slackchannel.SendSlackMessage(xr, api, channelID, q, f.log) - deployment := f.transformK8sResource(input, f.log) desired[resource.Name(deployment.GetName())] = &resource.DesiredComposed{Resource: &deployment} + desired[resource.Name(svc.GetName())] = &resource.DesiredComposed{Resource: &svc} + desired[resource.Name(ingress.GetName())] = &resource.DesiredComposed{Resource: &ingress} - if err := response.SetDesiredComposedResources(rsp, desired); err != nil { - return rsp, err - } } - err = response.SetDesiredCompositeResource(rsp, xr) - if err != nil { + + cronjob := composed.Unstructured{ + Unstructured: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kubernetes.crossplane.io/v1alpha2", + "kind": "Object", + "metadata": map[string]interface{}{ + "name": "slack-notify-cronjob", + }, + "spec": map[string]interface{}{ + "forProvider": map[string]interface{}{ + "manifest": map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "CronJob", + "metadata": map[string]interface{}{ + "name": "slack-notify-cronjob", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "schedule": schedule, + "timeZone": "Europe/Chisinau", + "jobTemplate": map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "restartPolicy": "OnFailure", + "serviceAccountName": input.ServiceAccountName, + "containers": []interface{}{ + map[string]interface{}{ + "name": "poll-container", + "image": input.CronJobImage, + "env": []interface{}{ + map[string]interface{}{ + "name": "SLACK_NOTIFY_MESSAGE", + "value": question, + }, + map[string]interface{}{ + "name": "POLL_NAME", + "value": pollName, + }, + map[string]interface{}{ + "name": "POLL_TITLE", + "value": pollTitle, + }, + }, + "envFrom": []interface{}{ + map[string]interface{}{ + "secretRef": map[string]interface{}{ + "name": secretName, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "providerConfigRef": map[string]interface{}{"name": input.ProviderConfigRef}, + }, + }, + }, + } + desired[resource.Name(cronjob.GetName())] = &resource.DesiredComposed{Resource: &cronjob} + + if err := response.SetDesiredComposedResources(rsp, desired); err != nil { return rsp, err } return rsp, nil diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index 5e19f61..eaecf36 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -22,7 +22,9 @@ type Input struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - ProviderConfigRef string `json:"providerConfigRef"` - DeploymentName string `json:"deploymentName"` - DeploymentImage string `json:"deploymentImage"` + ProviderConfigRef string `json:"providerConfigRef"` + DeploymentName string `json:"deploymentName"` + DeploymentImage string `json:"deploymentImage"` + ServiceAccountName string `json:"serviceAccountName"` + CronJobImage string `json:"cronJobImage"` } diff --git a/internal/slack-collector/Dockerfile b/internal/slack-collector/Dockerfile index e015bc5..396352d 100644 --- a/internal/slack-collector/Dockerfile +++ b/internal/slack-collector/Dockerfile @@ -1,12 +1,12 @@ -FROM golang:1.20-alpine as build -LABEL org.opencontainers.image.source https://github.com/kndpio/kndp +FROM golang:1.23-alpine as build +LABEL org.opencontainers.image.source=https://github.com/kndpio/function-poll WORKDIR /build -COPY ./packages/meal/slack-collector . +COPY ./internal/slack-collector . RUN go mod tidy RUN go build -o slack-collector -FROM golang:1.20-alpine +FROM golang:1.23-alpine WORKDIR /app COPY --from=build /build/slack-collector /app/ EXPOSE 3000 diff --git a/internal/slack-collector/main.go b/internal/slack-collector/main.go index 5d679ca..50a8fe0 100644 --- a/internal/slack-collector/main.go +++ b/internal/slack-collector/main.go @@ -56,21 +56,25 @@ type Poll struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec struct { - DeliveryTime string `json:"deliveryTime"` - DueOrderTime string `json:"dueOrderTime"` - DueTakeTime string `json:"dueTakeTime"` + DeliveryTime int64 `json:"deliveryTime"` + DueOrderTime int64 `json:"dueOrderTime"` + DueTakeTime int64 `json:"dueTakeTime"` + Schedule string `json:"schedule"` Voters []Voter `json:"voters"` Title string `json:"title"` Messages Message `json:"messages"` - Status string `json:"status"` } `json:"spec"` + Status struct { + Done bool `json:"done"` + LastNotificationTime int64 `json:"lastNotificationTime"` + } `json:"status"` } var ( - api = slack.New(os.Getenv("SLACK_API_TOKEN")) - path = os.Getenv("SLACK_COLLECTOR_PATH") - port = os.Getenv("SLACK_COLLECTOR_PORT") - pollResource *Poll + api = slack.New(os.Getenv("SLACK_API_TOKEN")) + path = os.Getenv("SLACK_COLLECTOR_PATH") + port = os.Getenv("SLACK_COLLECTOR_PORT") + response string ) // handleEventsEndpoint handles the events endpoint. @@ -107,8 +111,11 @@ func patchVoterStatus(user, pollSlackName, selectedOption string, dynamicClient Version: "v1alpha1", Resource: "polls", } - pollResource, _ = getK8sResource(dynamicClient, ctx, pollSlackName, resourceId) - + pollResource, err := getK8sResource(dynamicClient, ctx, pollSlackName, resourceId) + if err != nil { + return err + } + response = pollResource.Spec.Messages.Response foundUser := false for i := range pollResource.Spec.Voters { if pollResource.Spec.Voters[i].Name == user { @@ -128,9 +135,11 @@ func patchVoterStatus(user, pollSlackName, selectedOption string, dynamicClient pollResource.GetObjectMeta().SetManagedFields(nil) pollBytes, _ := json.Marshal(pollResource) - _, err := dynamicClient.Resource(resourceId).Namespace("").Patch(ctx, pollResource.GetObjectMeta().GetName(), types.MergePatchType, pollBytes, metav1.PatchOptions{FieldManager: "slack-collector"}) - - return err + _, err = dynamicClient.Resource(resourceId).Namespace("").Patch(ctx, pollResource.GetObjectMeta().GetName(), types.MergePatchType, pollBytes, metav1.PatchOptions{FieldManager: "slack-collector"}) + if err != nil { + fmt.Println("Error patching poll resource", err) + } + return nil } // getK8sResource gets the Kubernetes resource. @@ -158,16 +167,16 @@ func getK8sResource(dynamicClient dynamic.Interface, ctx context.Context, pollSl // respondMsg sends a response message to Slack. func respondMsg(userID string, userName string, selectedOption string, pollName string) { + attachment := slack.Attachment{ Color: "#f9a41b", CallbackID: pollName, - Text: pollResource.Spec.Messages.Response + "\n Selected: " + selectedOption, + Text: response + "\n Selected: " + selectedOption, Fields: []slack.AttachmentField{}, Actions: []slack.AttachmentAction{}, MarkdownIn: []string{}, Blocks: slack.Blocks{}, } - channelID, _, err := api.PostMessage( userID, slack.MsgOptionText("", true), diff --git a/internal/slack-notify/Dockerfile b/internal/slack-notify/Dockerfile new file mode 100644 index 0000000..7408346 --- /dev/null +++ b/internal/slack-notify/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.23-alpine as build +LABEL org.opencontainers.image.source=https://github.com/kndpio/function-poll + +WORKDIR /build +COPY ./internal/slack-notify . +RUN go mod tidy +RUN go build -o slack-notify + +FROM golang:1.23-alpine +WORKDIR /app +COPY --from=build /build/slack-notify /app/ +CMD [ "./slack-notify" ] diff --git a/internal/slack-notify/go.mod b/internal/slack-notify/go.mod new file mode 100644 index 0000000..ff7edd3 --- /dev/null +++ b/internal/slack-notify/go.mod @@ -0,0 +1,45 @@ +module slack-notify + +go 1.23.1 + +require ( + github.com/crossplane/function-sdk-go v0.3.0 + github.com/nlopes/slack v0.6.0 + k8s.io/apimachinery v0.31.1 + k8s.io/client-go v0.30.0 + sigs.k8s.io/controller-runtime v0.18.2 +) + +require ( + github.com/crossplane/crossplane-runtime v1.17.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.5.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/internal/slack-notify/go.sum b/internal/slack-notify/go.sum new file mode 100644 index 0000000..d0ee2c5 --- /dev/null +++ b/internal/slack-notify/go.sum @@ -0,0 +1,175 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/crossplane/crossplane-runtime v1.17.0 h1:y+GvxPT1M9s8BKt2AeZJdd2d6pg2xZeCO6LiR+VxEF8= +github.com/crossplane/crossplane-runtime v1.17.0/go.mod h1:vtglCrnnbq2HurAk9yLHa4qS0bbnCxaKL7C21cQcB/0= +github.com/crossplane/function-sdk-go v0.3.0 h1:ezutyOxtRXhIMSB93mzyp8pc4G7N9e9SRs5KqW5x6sU= +github.com/crossplane/function-sdk-go v0.3.0/go.mod h1:bvJQih3IbrNOSiQWzdkVhOpR+BHL125jTBqFyEYJxIE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= +github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689 h1:hNwajDgT0MlsxZzlUajZVmUYFpts8/CYe4BSNx503ZE= +google.golang.org/protobuf v1.34.3-0.20240816073751-94ecbc261689/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= +k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.18.2 h1:RqVW6Kpeaji67CY5nPEfRz6ZfFMk0lWQlNrLqlNpx+Q= +sigs.k8s.io/controller-runtime v0.18.2/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/slack-notify/main.go b/internal/slack-notify/main.go new file mode 100644 index 0000000..f93f88f --- /dev/null +++ b/internal/slack-notify/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/nlopes/slack" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + ctrl "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +var ( + token = os.Getenv("SLACK_API_TOKEN") + channelID = os.Getenv("SLACK_CHANEL_ID") + pollName = os.Getenv("POLL_NAME") + pollTitle = os.Getenv("POLL_TITLE") + slackNotifyMessage = os.Getenv("SLACK_NOTIFY_MESSAGE") +) + +// Voter represents the structure of an Voter reference. +type Voter struct { + Name string `json:"name"` + Status string `json:"status"` +} + +// Message represents the structure of a message. +type Message struct { + Question string `json:"question"` + Response string `json:"response"` + Result string `json:"result"` +} + +// Poll represents the structure of a poll. +type Poll struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec struct { + DeliveryTime int64 `json:"deliveryTime"` + DueOrderTime int64 `json:"dueOrderTime"` + DueTakeTime int64 `json:"dueTakeTime"` + Schedule string `json:"schedule"` + Voters []Voter `json:"voters"` + Title string `json:"title"` + Messages Message `json:"messages"` + } `json:"spec"` + Status struct { + Done bool `json:"done"` + LastNotificationTime int64 `json:"lastNotificationTime"` + } `json:"status"` +} + +// getK8sResource gets the Kubernetes resource. +func getK8sResource(dynamicClient dynamic.Interface, ctx context.Context, pollSlackName string, resId schema.GroupVersionResource) (*Poll, error) { + + res, err := dynamicClient.Resource(resId).Namespace(""). + List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, item := range res.Items { + res := &Poll{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, res); err != nil { + return nil, fmt.Errorf("error converting Unstructured to Poll struct: %v", err) + } + if res.GetObjectMeta().GetName() == pollSlackName { + return res, nil + } + } + return nil, fmt.Errorf("poll resource with name %s not found", pollSlackName) +} + +func main() { + config, err := ctrl.GetConfig() + if err != nil { + fmt.Println("error getting config", err) + } + client, err := dynamic.NewForConfig(config) + if err != nil { + fmt.Println("error getting client", err) + } + resourceId := schema.GroupVersionResource{ + Group: "kndp.io", + Version: "v1alpha1", + Resource: "polls", + } + + pollResource, _ := getK8sResource(client, context.Background(), pollName, resourceId) + pollResource.GetObjectMeta().SetManagedFields(nil) + pollBytes, _ := json.Marshal(pollResource) + _, err = client.Resource(resourceId).Namespace("").Patch(context.Background(), pollResource.GetObjectMeta().GetName(), types.MergePatchType, pollBytes, metav1.PatchOptions{FieldManager: "slack-collector"}) + if err != nil { + fmt.Println("Error patching poll resource", err) + } + + statusBytes, _ := json.Marshal(map[string]interface{}{ + "status": map[string]interface{}{ + "done": false, + "lastNotificationTime": time.Now().Unix(), + }, + }) + + // Use the "/status" subresource to update just the status + _, err = client.Resource(resourceId).Namespace("").Patch( + context.Background(), + pollResource.GetObjectMeta().GetName(), + types.MergePatchType, + statusBytes, + metav1.PatchOptions{FieldManager: "slack-collector"}, + "/status", + ) + if err != nil { + fmt.Println("Error patching poll status", err) + } + + api := slack.New(token) + + members, _, err := api.GetUsersInConversation(&slack.GetUsersInConversationParameters{ + ChannelID: channelID, + }) + if err != nil { + fmt.Println("error getting users in conversation", err) + } + + for _, memberID := range members { + userInfo, err := api.GetUserInfo(memberID) + if err != nil { + fmt.Println("error getting user info for", memberID, err) + continue + } + + attachment := slack.Attachment{ + Color: "#f9a41b", + CallbackID: pollName, + Title: pollTitle, + TitleLink: pollTitle, + Text: slackNotifyMessage, + Fields: []slack.AttachmentField{}, + Actions: []slack.AttachmentAction{{Name: "actionSelect", Type: "select", Options: []slack.AttachmentActionOption{{Text: "Yes", Value: "Yes"}, {Text: "No", Value: "No"}}}, {Name: "actionCancel", Text: "Cancel", Type: "button", Style: "danger"}}, + } + + channelID, _, err := api.PostMessage( + userInfo.ID, + slack.MsgOptionText("", true), + slack.MsgOptionAttachments(attachment), + slack.MsgOptionAsUser(true), + ) + if err != nil { + fmt.Println("error sending message to user in channel: ", userInfo.Name, channelID, err) + } else { + fmt.Println("message sent to user in channel: ", userInfo.Name, channelID) + } + + } +} diff --git a/internal/slackchannel/slackchannel.go b/internal/slackchannel/slackchannel.go index 2978868..5d816fb 100644 --- a/internal/slackchannel/slackchannel.go +++ b/internal/slackchannel/slackchannel.go @@ -17,7 +17,6 @@ import ( var ( channelId = os.Getenv("SLACK_CHANEL_ID") - pollName string pollTitle string ) @@ -39,14 +38,18 @@ type Poll struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec struct { - DeliveryTime string `json:"deliveryTime"` - DueOrderTime string `json:"dueOrderTime"` - DueTakeTime string `json:"dueTakeTime"` + DeliveryTime int64 `json:"deliveryTime"` + DueOrderTime int64 `json:"dueOrderTime"` + DueTakeTime int64 `json:"dueTakeTime"` + Schedule string `json:"schedule"` Voters []Voter `json:"voters"` Title string `json:"title"` Messages Message `json:"messages"` - Status string `json:"status"` } `json:"spec"` + Status struct { + Done bool `json:"done"` + LastNotificationTime int64 `json:"lastNotificationTime"` + } `json:"status"` } // UserVoted checks if the user should receive a message based on status @@ -80,59 +83,6 @@ func ProcessSlackMembers(api *slack.Client, channelID string, logger logging.Log return realUsers, nil } -// SendSlackMessage is for sending messages to users in slack -func SendSlackMessage(xr *resource.Composite, api *slack.Client, channelID string, slackNotifyMessage string, logger logging.Logger) { - members, _, err := api.GetUsersInConversation(&slack.GetUsersInConversationParameters{ - ChannelID: channelID, - }) - if err != nil { - logger.Info("error getting conversation members", "warning", err) - } - - logger.Debug("conversation members:", "userId", members) - pollName, _ = xr.Resource.GetString("metadata.name") - pollTitle, _ = xr.Resource.GetString("spec.title") - poll := Poll{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(xr.Resource.Object, &poll); err != nil { - logger.Info("error converting Unstructured to Poll", "warning", err) - } - - for _, memberID := range members { - userInfo, err := api.GetUserInfo(memberID) - if err != nil { - logger.Info("error getting user info for", memberID, err) - continue - } - - attachment := slack.Attachment{ - Color: "#f9a41b", - CallbackID: pollName, - Title: pollTitle, - TitleLink: pollTitle, - Text: slackNotifyMessage, - Fields: []slack.AttachmentField{}, - Actions: []slack.AttachmentAction{{Name: "actionSelect", Type: "select", Options: []slack.AttachmentActionOption{{Text: "Yes", Value: "Yes"}, {Text: "No", Value: "No"}}}, {Name: "actionCancel", Text: "Cancel", Type: "button", Style: "danger"}}, - MarkdownIn: []string{}, - Blocks: slack.Blocks{}, - } - - if UserVoted(poll.Spec.Voters, userInfo.Name) { - channelID, _, err := api.PostMessage( - userInfo.ID, - slack.MsgOptionText("", true), - slack.MsgOptionAttachments(attachment), - slack.MsgOptionAsUser(true), - ) - if err != nil { - logger.Info("error sending message to user: ", userInfo.Name, userInfo.ID, err) - } else { - logger.Debug("message sent to user in channel: ", userInfo.Name, channelID) - } - - } - } -} - func countUsers(voters []Voter) int { count := 0 for _, voter := range voters { @@ -144,7 +94,7 @@ func countUsers(voters []Voter) int { } // SlackOrder sends an order notification via Slack. -func SlackOrder(input *v1beta1.Input, api *slack.Client, xr *resource.Composite, logger logging.Logger) { +func SlackOrder(input *v1beta1.Input, api *slack.Client, xr *resource.Composite, logger logging.Logger, resultText string) *resource.Composite { pollTitle, _ = xr.Resource.GetString("spec.title") poll := Poll{} @@ -158,7 +108,7 @@ func SlackOrder(input *v1beta1.Input, api *slack.Client, xr *resource.Composite, CallbackID: pollTitle, Title: pollTitle, TitleLink: pollTitle, - Text: "Total votes: " + strconv.Itoa(textContent), + Text: resultText + strconv.Itoa(textContent), MarkdownIn: []string{}, } @@ -168,10 +118,16 @@ func SlackOrder(input *v1beta1.Input, api *slack.Client, xr *resource.Composite, slack.MsgOptionAttachments(attachment), slack.MsgOptionAsUser(true), ) + if err != nil { logger.Info("error sending slack message", "warning", err) } else { logger.Info("message successfully sent to channel", channelID, timestamp) } - + poll.Spec.Voters = nil + xr.Resource.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&poll) + if err != nil { + logger.Info("error converting Poll to Unstructured:", err) + } + return xr } diff --git a/package/composition.yaml b/package/composition.yaml deleted file mode 100644 index d182f0d..0000000 --- a/package/composition.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apiextensions.crossplane.io/v1 -kind: Composition -metadata: - name: poll-composition -spec: - compositeTypeRef: - apiVersion: kndp.io/v1alpha1 - kind: Poll - mode: Pipeline - pipeline: - - step: run-the-template - functionRef: - name: function-template-go - input: - providerConfigRef: "default" - deploymentImage: "ghcr.io/kndpio/kndp/slack-collector:0.1.0" - deploymentName: "collect" diff --git a/package/crossplane.yaml b/package/crossplane.yaml index 78e3be0..66cb7b7 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -4,3 +4,5 @@ kind: Function metadata: name: poll-function spec: {} + + diff --git a/package/poll.yaml b/package/poll.yaml index dff84f4..917651c 100644 --- a/package/poll.yaml +++ b/package/poll.yaml @@ -28,15 +28,15 @@ spec: type: string type: object dueOrderTime: - type: string + type: integer dueTakeTime: - type: string + type: integer deliveryTime: + type: integer + schedule: type: string title: type: string - status: - type: string messages: type: object properties: @@ -46,5 +46,10 @@ spec: type: string result: type: string - - + status: + type: object + properties: + done: + type: boolean + lastNotificationTime: + type: integer