From 4128d942648bd7d2d56f35585217ef22893794be Mon Sep 17 00:00:00 2001 From: Maksim Paskal Date: Mon, 29 Jan 2024 10:50:41 +0000 Subject: [PATCH] extend webhook data Signed-off-by: Maksim Paskal --- .gitignore | 3 +- Makefile | 2 +- README.md | 78 ++++++++++++-- .../aks-node-termination-handler/Chart.yaml | 2 +- .../templates/configmap.yaml | 8 ++ .../templates/daemonset.yaml | 16 ++- .../aks-node-termination-handler/values.yaml | 18 ++++ e2e/main_test.go | 4 +- pkg/alert/alert.go | 16 +-- pkg/cache/cache_test.go | 2 +- pkg/config/config.go | 8 +- pkg/events/events.go | 31 ++++-- pkg/metrics/metrics_test.go | 2 +- pkg/template/README.md | 23 ++++ pkg/template/template.go | 101 ++++++++++++++++-- pkg/template/template_test.go | 100 ++++++++++++----- pkg/template/testdata/message.json | 62 +++++++++++ pkg/types/types.go | 18 ++-- pkg/webhook/testdata/WebhookTemplateFile.txt | 1 + pkg/webhook/webhook.go | 25 +++-- pkg/webhook/webhook_test.go | 40 +++++-- 21 files changed, 466 insertions(+), 94 deletions(-) create mode 100644 charts/aks-node-termination-handler/templates/configmap.yaml create mode 100644 pkg/template/README.md create mode 100644 pkg/template/testdata/message.json create mode 100644 pkg/webhook/testdata/WebhookTemplateFile.txt diff --git a/.gitignore b/.gitignore index eeeeff7..1dc60ea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ kubeconfig dist /aks-node-termination-handler simulateEviction -coverage.out \ No newline at end of file +coverage.out +*.tmp \ No newline at end of file diff --git a/Makefile b/Makefile index 3b551c8..e8db42b 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ run: -gracePeriodSeconds=0 \ -endpoint=http://localhost:28080/pkg/types/testdata/ScheduledEventsType.json \ -webhook.url=http://localhost:9091/metrics/job/aks-node-termination-handler \ - -webhook.template='node_termination_event{node="{{ .Node }}"} 1' \ + -webhook.template='node_termination_event{node="{{ .NodeName }}"} 1' \ -telegram.token=${telegramToken} \ -telegram.chatID=${telegramChatID} \ -web.address=127.0.0.1:17923 diff --git a/README.md b/README.md index 3aabcff..2b28e5c 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,12 @@ aks-node-termination-handler/aks-node-termination-handler \ --set priorityClassName=system-node-critical ``` -## Alerting +## Send notification events -To make alerts to Telegram or Slack or Webhook +You can compose your payload with markers that described [here](pkg/template/README.md) + +
+ Send Telegram notification ```bash helm upgrade aks-node-termination-handler \ @@ -82,11 +85,74 @@ helm upgrade aks-node-termination-handler \ --namespace kube-system \ aks-node-termination-handler/aks-node-termination-handler \ --set priorityClassName=system-node-critical \ ---set args[0]=-telegram.token= \ ---set args[1]=-telegram.chatID= \ ---set args[2]=-webhook.url=http://prometheus-pushgateway.prometheus.svc.cluster.local:9091/metrics/job/aks-node-termination-handler \ ---set args[3]=-webhook.template='node_termination_event{node="{{ .Node }}"} 1' +--set 'args[0]=-telegram.token=' \ +--set 'args[1]=-telegram.chatID=' +``` +
+ +
+ Send Slack notification + +```bash +# create payload file +cat < + +
+ Send Prometheus Pushgateway event + +```bash +cat < ## Simulate eviction diff --git a/charts/aks-node-termination-handler/Chart.yaml b/charts/aks-node-termination-handler/Chart.yaml index 92cbde5..0f0ec79 100644 --- a/charts/aks-node-termination-handler/Chart.yaml +++ b/charts/aks-node-termination-handler/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 icon: https://helm.sh/img/helm.svg name: aks-node-termination-handler -version: 1.1.1 +version: 1.1.2 description: Gracefully handle Azure Virtual Machines shutdown within Kubernetes maintainers: - name: maksim-paskal # Maksim Paskal diff --git a/charts/aks-node-termination-handler/templates/configmap.yaml b/charts/aks-node-termination-handler/templates/configmap.yaml new file mode 100644 index 0000000..3810e17 --- /dev/null +++ b/charts/aks-node-termination-handler/templates/configmap.yaml @@ -0,0 +1,8 @@ +{{ if .Values.configMap.create }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.configMap.name }} +data: +{{ toYaml .Values.configMap.data | indent 2 }} +{{ end }} \ No newline at end of file diff --git a/charts/aks-node-termination-handler/templates/daemonset.yaml b/charts/aks-node-termination-handler/templates/daemonset.yaml index 3f05789..92faa58 100644 --- a/charts/aks-node-termination-handler/templates/daemonset.yaml +++ b/charts/aks-node-termination-handler/templates/daemonset.yaml @@ -36,6 +36,13 @@ spec: nodeSelector: {{- toYaml .Values.nodeSelector | nindent 8 }} {{- end }} + volumes: + - name: files + configMap: + name: {{ .Values.configMap.name }} + {{ if .Values.extraVolumes }} + {{ toYaml .Values.extraVolumes | indent 6 }} + {{ end }} containers: - name: aks-node-termination-handler resources: @@ -75,4 +82,11 @@ spec: ports: - name: http containerPort: 17923 - protocol: TCP \ No newline at end of file + protocol: TCP + volumeMounts: + - name: files + mountPath: {{ .Values.configMap.mountPath }} + readOnly: true + {{ if .Values.extraVolumeMounts}} + {{ toYaml .Values.extraVolumeMounts | indent 8 }} + {{ end }} \ No newline at end of file diff --git a/charts/aks-node-termination-handler/values.yaml b/charts/aks-node-termination-handler/values.yaml index 2158229..22bbec2 100644 --- a/charts/aks-node-termination-handler/values.yaml +++ b/charts/aks-node-termination-handler/values.yaml @@ -8,6 +8,24 @@ priorityClassName: "" annotations: {} labels: {} +configMap: + create: true + name: aks-node-termination-handler-files + mountPath: /files + data: {} + # slack-payload.json: | + # { + # "channel": "#mychannel", + # "username": "webhookbot", + # "text": "This is message for {{ .NodeName }}, {{ .InstanceType }} from {{ .NodeRegion }}", + # "icon_emoji": ":ghost:" + # } + # prometheus-pushgateway-payload.txt: | + # node_termination_event{node="{{ .NodeName }}"} 1 + +extraVolumes: [] +extraVolumeMounts: [] + metrics: addAnnotations: true diff --git a/e2e/main_test.go b/e2e/main_test.go index cb79652..3c8f646 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -25,7 +25,7 @@ import ( "github.com/maksim-paskal/aks-node-termination-handler/pkg/types" ) -var ctx = context.Background() +var ctx = context.TODO() func TestDrain(t *testing.T) { t.Parallel() @@ -46,7 +46,7 @@ func TestDrain(t *testing.T) { t.Fatal(err) } - if err := alert.SendTelegram(template.MessageType{Template: "e2e"}); err != nil { + if err := alert.SendTelegram(&template.MessageType{Template: "e2e"}); err != nil { t.Fatal(err) } diff --git a/pkg/alert/alert.go b/pkg/alert/alert.go index a6cd82b..c0a65f6 100644 --- a/pkg/alert/alert.go +++ b/pkg/alert/alert.go @@ -13,13 +13,11 @@ limitations under the License. package alert import ( - "context" "strconv" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/maksim-paskal/aks-node-termination-handler/pkg/config" "github.com/maksim-paskal/aks-node-termination-handler/pkg/template" - "github.com/maksim-paskal/aks-node-termination-handler/pkg/webhook" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -56,19 +54,7 @@ func Ping() error { return nil } -func SendALL(ctx context.Context, obj template.MessageType) error { - if err := SendTelegram(obj); err != nil { - return errors.Wrap(err, "error in sending to telegram") - } - - if err := webhook.SendWebHook(ctx, obj); err != nil { - return errors.Wrap(err, "error in sending to webhook") - } - - return nil -} - -func SendTelegram(obj template.MessageType) error { +func SendTelegram(obj *template.MessageType) error { if len(*config.Get().TelegramToken) == 0 { return nil } diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 21bb063..3aea432 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -23,7 +23,7 @@ import ( func TestCache(t *testing.T) { t.Parallel() - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(context.TODO()) defer cancel() go cache.SheduleCleaning(ctx) diff --git a/pkg/config/config.go b/pkg/config/config.go index eacc165..2ae5d3b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,13 +27,13 @@ import ( const ( azureEndpoint = "http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01" - defaultAlertMessage = "Draining node={{ .Node }}, type={{ .Event.EventType }}" + defaultAlertMessage = "Draining node={{ .NodeName }}, type={{ .Event.EventType }}" defaultPeriod = 5 * time.Second defaultPodGracePeriodSeconds = -1 defaultNodeGracePeriodSeconds = 120 defaultGracePeriodSecond = 10 defaultRequestTimeout = 1 * time.Second - defaultWebHookTimeout = 5 * time.Second + defaultWebHookTimeout = 30 * time.Second ) var ( @@ -58,6 +58,7 @@ type Type struct { WebHookContentType *string WebHookURL *string WebHookTemplate *string + WebHookTemplateFile *string WebHookMethod *string WebHookTimeout *time.Duration SentryDSN *string @@ -86,7 +87,8 @@ var config = Type{ WebHookContentType: flag.String("webhook.contentType", "application/json", "request content-type header"), WebHookURL: flag.String("webhook.url", os.Getenv("WEBHOOK_URL"), "send alerts to webhook"), WebHookTimeout: flag.Duration("webhook.timeout", defaultWebHookTimeout, "request timeout"), - WebHookTemplate: flag.String("webhook.template", "test", "request body"), + WebHookTemplate: flag.String("webhook.template", os.Getenv("WEBHOOK_TEMPLATE"), "request body"), + WebHookTemplateFile: flag.String("webhook.template-file", os.Getenv("WEBHOOK_TEMPLATE_FILE"), "path to request body template file"), //nolint:lll SentryDSN: flag.String("sentry.dsn", "", "sentry DSN"), WebHTTPAddress: flag.String("web.address", ":17923", ""), TaintNode: flag.Bool("taint.node", false, "Taint the node before cordon and draining"), diff --git a/pkg/events/events.go b/pkg/events/events.go index 5b2154f..a193b86 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -27,6 +27,7 @@ import ( "github.com/maksim-paskal/aks-node-termination-handler/pkg/template" "github.com/maksim-paskal/aks-node-termination-handler/pkg/types" "github.com/maksim-paskal/aks-node-termination-handler/pkg/utils" + "github.com/maksim-paskal/aks-node-termination-handler/pkg/webhook" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -131,13 +132,8 @@ func readEndpoint(ctx context.Context, azureResource string) (bool, error) { //n continue } - err := alert.SendALL(ctx, template.MessageType{ - Event: event, - Node: azureResource, - Template: *config.Get().AlertMessage, - }) - if err != nil { - log.WithError(err).Error("error in alerts.Send") + if err := sendEvent(ctx, event); err != nil { + log.WithError(err).Error("error in sendEvent") } err = api.DrainNode(ctx, *config.Get().NodeName, string(event.EventType), event.EventId) @@ -159,3 +155,24 @@ func getsharedMetricsLabels(resourceName string) []string { resourceName, } } + +func sendEvent(ctx context.Context, event types.ScheduledEventsEvent) error { + message, err := template.NewMessageType(ctx, *config.Get().NodeName, event) + if err != nil { + return errors.Wrap(err, "error in template.NewMessageType") + } + + log.Infof("Message: %+v", message) + + message.Template = *config.Get().AlertMessage + + if err := alert.SendTelegram(message); err != nil { + log.WithError(err).Error("error in alert.SendTelegram") + } + + if err := webhook.SendWebHook(ctx, message); err != nil { + log.WithError(err).Error("error in webhook.SendWebHook") + } + + return nil +} diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 9fee30c..8ccb385 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -27,7 +27,7 @@ import ( var ( client = &http.Client{} ts = httptest.NewServer(metrics.GetHandler()) - ctx = context.Background() + ctx = context.TODO() ) func TestMetricsInc(t *testing.T) { diff --git a/pkg/template/README.md b/pkg/template/README.md new file mode 100644 index 0000000..2710b5d --- /dev/null +++ b/pkg/template/README.md @@ -0,0 +1,23 @@ +# Templating Options + +| Template | Description | Example | +| --------- | ----------- | ------- | +| `{{ .Event.EventId }}` | Globally unique identifier for this event. | 602d9444-d2cd-49c7-8624-8643e7171297 | +| `{{ .Event.EventType }}` | Impact this event causes. | Reboot | +| `{{ .Event.ResourceType }}` | Type of resource this event affects. | VirtualMachine | +| `{{ .Event.Resources }}` | List of resources this event affects. | [ FrontEnd_IN_0 ...] | +| `{{ .Event.EventStatus }}` | Status of this event. | Scheduled | +| `{{ .Event.NotBefore }}` | Time after which this event can start. The event is guaranteed to not start before this time. Will be blank if the event has already started | Mon, 19 Sep 2016 18:29:47 GMT | +| `{{ .Event.Description }}` | Description of this event. | Host server is undergoing maintenance | +| `{{ .Event.EventSource }}` | Initiator of the event. | Platform | +| `{{ .Event.DurationInSeconds }}` | The expected duration of the interruption caused by the event. | -1 | +| `{{ .NodeLabels }}` | Node labels | kubernetes.azure.com/agentpool:spotcpu4m16n ... | +| `{{ .NodeName }}` | Node name | aks-spotcpu4m16n-41289323-vmss0000ny | +| `{{ .ClusterName }}` | Node label kubernetes.azure.com/cluster | MC_EAST-US-RC-STAGE_stage-cluster_eastus | +| `{{ .InstanceType }}` | Node label node.kubernetes.io/instance-type | Standard_D4as_v5 | +| `{{ .NodeArch }}` | Node label kubernetes.io/arch | amd64 | +| `{{ .NodeOS }}` | Node label kubernetes.io/os | linux | +| `{{ .NodeRole }}` | Node label kubernetes.io/role | agent | +| `{{ .NodeRegion }}` | Node label topology.kubernetes.io/region | eastus | +| `{{ .NodeZone }}` | Node label topology.kubernetes.io/zone | 0 | +| `{{ .NodePods }}` | List of pods on node | [ pod1 ...] | diff --git a/pkg/template/template.go b/pkg/template/template.go index 8cae0ad..9e8d196 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -14,20 +14,109 @@ package template import ( "bytes" + "context" "html/template" + "github.com/maksim-paskal/aks-node-termination-handler/pkg/client" "github.com/maksim-paskal/aks-node-termination-handler/pkg/types" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type MessageType struct { - Node string - Event types.ScheduledEventsEvent - Template string - NewLine string // Used to making new line in templating results. Readonly. + Event types.ScheduledEventsEvent + Template string + NodeLabels map[string]string `description:"Node labels"` + NodeName string `description:"Node name"` + ClusterName string `description:"Node label kubernetes.azure.com/cluster"` + InstanceType string `description:"Node label node.kubernetes.io/instance-type"` + NodeArch string `description:"Node label kubernetes.io/arch"` + NodeOS string `description:"Node label kubernetes.io/os"` + NodeRole string `description:"Node label kubernetes.io/role"` + NodeRegion string `description:"Node label topology.kubernetes.io/region"` + NodeZone string `description:"Node label topology.kubernetes.io/zone"` + NodePods []string `description:"List of pods on node"` } -func Message(obj MessageType) (string, error) { +func getNodeLabels(ctx context.Context, nodeName string) (map[string]string, error) { + // this need for unit tests + if client.GetKubernetesClient() == nil { + return make(map[string]string), nil + } + + node, err := client.GetKubernetesClient().CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrap(err, "error in nodes.get") + } + + return node.Labels, nil +} + +func getPodReferenceKind(pod corev1.Pod) string { + for _, ownerReference := range pod.OwnerReferences { + if len(ownerReference.Kind) > 0 { + return ownerReference.Kind + } + } + + return "" +} + +func getNodePods(ctx context.Context, nodeName string) ([]string, error) { + // this need for unit tests + if client.GetKubernetesClient() == nil { + return []string{}, nil + } + + pods, err := client.GetKubernetesClient().CoreV1().Pods("").List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "error in pods.list") + } + + result := make([]string, 0) + + for _, pod := range pods.Items { + // ignore DaemonSet pods from pods list, because they are not affected by node termination + if getPodReferenceKind(pod) == "DaemonSet" { + continue + } + + if pod.Spec.NodeName == nodeName { + result = append(result, pod.Name) + } + } + + return result, nil +} + +func NewMessageType(ctx context.Context, nodeName string, event types.ScheduledEventsEvent) (*MessageType, error) { + nodeLabels, err := getNodeLabels(ctx, nodeName) + if err != nil { + return nil, errors.Wrap(err, "error in nodes.get") + } + + nodePods, err := getNodePods(ctx, nodeName) + if err != nil { + return nil, errors.Wrap(err, "error in getNodePods") + } + + return &MessageType{ + Event: event, + NodeName: nodeName, + NodeLabels: nodeLabels, + ClusterName: nodeLabels["kubernetes.azure.com/cluster"], + InstanceType: nodeLabels["node.kubernetes.io/instance-type"], + NodeArch: nodeLabels["kubernetes.io/arch"], + NodeOS: nodeLabels["kubernetes.io/os"], + NodeRole: nodeLabels["kubernetes.io/role"], + NodeRegion: nodeLabels["topology.kubernetes.io/region"], + NodeZone: nodeLabels["topology.kubernetes.io/zone"], + NodePods: nodePods, + }, nil +} + +func Message(obj *MessageType) (string, error) { tmpl, err := template.New("message").Parse(obj.Template) if err != nil { return "", errors.Wrap(err, "error in template.Parse") @@ -35,8 +124,6 @@ func Message(obj MessageType) (string, error) { var tpl bytes.Buffer - obj.NewLine = "\n" - err = tmpl.Execute(&tpl, obj) if err != nil { return "", errors.Wrap(err, "error in template.Execute") diff --git a/pkg/template/template_test.go b/pkg/template/template_test.go index 77344f2..04d4e12 100644 --- a/pkg/template/template_test.go +++ b/pkg/template/template_test.go @@ -13,6 +13,11 @@ limitations under the License. package template_test import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" "testing" "github.com/maksim-paskal/aks-node-termination-handler/pkg/template" @@ -24,12 +29,13 @@ const fakeTemplate = "{{" func TestTemplateMessage(t *testing.T) { t.Parallel() - obj := template.MessageType{ + obj := &template.MessageType{ Event: types.ScheduledEventsEvent{ EventId: "someID", EventType: "someType", }, - Template: "test {{ .Event.EventId }} {{ .Event.EventType }}", + NodePods: []string{"pod1", "pod2"}, + Template: "test {{ .Event.EventId }} {{ .Event.EventType }} {{ .NodePods}}", } tpl, err := template.Message(obj) @@ -37,28 +43,7 @@ func TestTemplateMessage(t *testing.T) { t.Fatal(err) } - if want := "test someID someType"; tpl != want { - t.Fatalf("want=%s,got=%s", want, tpl) - } -} - -func TestLineBreak(t *testing.T) { - t.Parallel() - - obj := template.MessageType{ - Event: types.ScheduledEventsEvent{ - EventId: "someID", - EventType: "someType", - }, - Template: `{{ .Event.EventId }}{{ .NewLine }}{{ .Event.EventType }}`, - } - - tpl, err := template.Message(obj) - if err != nil { - t.Fatal(err) - } - - if want := "someID\nsomeType"; tpl != want { + if want := "test someID someType [pod1 pod2]"; tpl != want { t.Fatalf("want=%s,got=%s", want, tpl) } } @@ -66,7 +51,7 @@ func TestLineBreak(t *testing.T) { func TestFakeTemplate(t *testing.T) { t.Parallel() - _, err := template.Message(template.MessageType{ + _, err := template.Message(&template.MessageType{ Template: fakeTemplate, }) if err == nil { @@ -77,7 +62,7 @@ func TestFakeTemplate(t *testing.T) { func TestFakeTemplateFunc(t *testing.T) { t.Parallel() - _, err := template.Message(template.MessageType{ + _, err := template.Message(&template.MessageType{ Template: "{{ .DDD }}", }) if err == nil { @@ -86,3 +71,66 @@ func TestFakeTemplateFunc(t *testing.T) { t.Log(err) } + +func TestTemplateMarkdown(t *testing.T) { + t.Parallel() + + message := template.MessageType{} + + messageBytes, err := os.ReadFile("testdata/message.json") + if err != nil { + t.Fatal(err) + } + + if err := json.Unmarshal(messageBytes, &message); err != nil { + t.Fatal(err) + } + + printType("", message) + + if err = os.WriteFile("README.md.tmp", []byte(buf.String()), 0o644); err != nil { //nolint:gosec + t.Fatal(err) + } +} + +var buf strings.Builder + +func printType(prefix string, message interface{}) { + v := reflect.ValueOf(message) + typeOfS := v.Type() + + for i := 0; i < v.NumField(); i++ { + switch typeOfS.Field(i).Name { + case "Template": + case "Event": + printType(typeOfS.Field(i).Name+".", v.Field(i).Interface()) + default: + value := v.Field(i).Interface() + + switch v.Field(i).Type().Kind() { //nolint:exhaustive + case reflect.Slice: + a := reflect.ValueOf(value).Interface().([]string) //nolint:forcetypeassert + if len(a) > 0 { + value = fmt.Sprintf("[ %s ...]", a[0]) + } + case reflect.Int: + value = fmt.Sprintf("%d", value) + case reflect.Map: + a := reflect.ValueOf(value).Interface().(map[string]string) //nolint:forcetypeassert + for k, v := range a { + value = fmt.Sprintf("%s:%s ...", k, v) + + break + } + } + + buf.WriteString(fmt.Sprintf( + "| `{{ .%s%s }}` | %v | %v |\n", + prefix, + typeOfS.Field(i).Name, + typeOfS.Field(i).Tag.Get("description"), + value, + )) + } + } +} diff --git a/pkg/template/testdata/message.json b/pkg/template/testdata/message.json new file mode 100644 index 0000000..d1274bc --- /dev/null +++ b/pkg/template/testdata/message.json @@ -0,0 +1,62 @@ +{ + "Event": { + "EventId": "602d9444-d2cd-49c7-8624-8643e7171297", + "EventType": "Reboot", + "ResourceType": "VirtualMachine", + "Resources": [ + "FrontEnd_IN_0", + "aks-spotcpu2d2as-24469130-vmss_1", + "aks-spotcpu4m16n-41289323-vmss_862" + ], + "EventStatus": "Scheduled", + "NotBefore": "Mon, 19 Sep 2016 18:29:47 GMT", + "Description": "Host server is undergoing maintenance", + "EventSource": "Platform", + "DurationInSeconds": -1 + }, + "Template": "", + "NodeLabels": { + "agentpool": "spotcpu4m16n", + "beta.kubernetes.io/arch": "amd64", + "beta.kubernetes.io/instance-type": "Standard_D4as_v5", + "beta.kubernetes.io/os": "linux", + "failure-domain.beta.kubernetes.io/region": "eastus", + "failure-domain.beta.kubernetes.io/zone": "0", + "kubernetes.azure.com/agentpool": "spotcpu4m16n", + "kubernetes.azure.com/cluster": "MC_EAST-US-RC-STAGE_stage-cluster_eastus", + "kubernetes.azure.com/consolidated-additional-properties": "d9a49827-aede-11ee-832c-fe2d222ef432", + "kubernetes.azure.com/kubelet-identity-client-id": "6781a919-9379-417c-8aff-257ecacd1139", + "kubernetes.azure.com/mode": "user", + "kubernetes.azure.com/network-policy": "none", + "kubernetes.azure.com/node-image-version": "AKSUbuntu-2204gen2containerd-202312.06.0", + "kubernetes.azure.com/nodepool-type": "VirtualMachineScaleSets", + "kubernetes.azure.com/os-sku": "Ubuntu", + "kubernetes.azure.com/role": "agent", + "kubernetes.azure.com/scalesetpriority": "spot", + "kubernetes.azure.com/storageprofile": "managed", + "kubernetes.azure.com/storagetier": "Premium_LRS", + "kubernetes.io/arch": "amd64", + "kubernetes.io/hostname": "aks-spotcpu4m16n-41289323-vmss0000ny", + "kubernetes.io/os": "linux", + "kubernetes.io/role": "agent", + "node-role.kubernetes.io/agent": "", + "node.kubernetes.io/instance-type": "Standard_D4as_v5", + "storageprofile": "managed", + "storagetier": "Premium_LRS", + "topology.disk.csi.azure.com/zone": "", + "topology.kubernetes.io/region": "eastus", + "topology.kubernetes.io/zone": "0" + }, + "NodeName": "aks-spotcpu4m16n-41289323-vmss0000ny", + "ClusterName": "MC_EAST-US-RC-STAGE_stage-cluster_eastus", + "InstanceType": "Standard_D4as_v5", + "NodeArch": "amd64", + "NodeOS": "linux", + "NodeRole": "agent", + "NodeRegion": "eastus", + "NodeZone": "0", + "NodePods": [ + "pod1", + "pod2" + ] +} \ No newline at end of file diff --git a/pkg/types/types.go b/pkg/types/types.go index fc8ac73..0422b3b 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -32,15 +32,15 @@ const ( // https://docs.microsoft.com/en-us/azure/virtual-machines/linux/scheduled-events type ScheduledEventsEvent struct { - EventId string //nolint:golint,revive,stylecheck - EventType ScheduledEventsEventType - ResourceType string - Resources []string - EventStatus string - NotBefore string // Mon, 19 Sep 2016 18:29:47 GMT - Description string - EventSource string - DurationInSeconds int + EventId string `description:"Globally unique identifier for this event."` //nolint:golint,revive,stylecheck,lll + EventType ScheduledEventsEventType `description:"Impact this event causes."` + ResourceType string `description:"Type of resource this event affects."` + Resources []string `description:"List of resources this event affects."` + EventStatus string `description:"Status of this event."` + NotBefore string `description:"Time after which this event can start. The event is guaranteed to not start before this time. Will be blank if the event has already started"` //nolint:lll + Description string `description:"Description of this event."` + EventSource string `description:"Initiator of the event."` + DurationInSeconds int `description:"The expected duration of the interruption caused by the event."` //nolint:lll } // api-version=2020-07-01. diff --git a/pkg/webhook/testdata/WebhookTemplateFile.txt b/pkg/webhook/testdata/WebhookTemplateFile.txt new file mode 100644 index 0000000..dbab63f --- /dev/null +++ b/pkg/webhook/testdata/WebhookTemplateFile.txt @@ -0,0 +1 @@ +node_termination_event{node="{{ .NodeName }}"} 1 \ No newline at end of file diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 2da0e81..73d4406 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -17,6 +17,7 @@ import ( "context" "fmt" "net/http" + "os" "github.com/maksim-paskal/aks-node-termination-handler/pkg/config" "github.com/maksim-paskal/aks-node-termination-handler/pkg/metrics" @@ -30,7 +31,7 @@ var client = &http.Client{ var errHTTPNotOK = errors.New("http result not OK") -func SendWebHook(ctx context.Context, obj template.MessageType) error { +func SendWebHook(ctx context.Context, obj *template.MessageType) error { ctx, cancel := context.WithTimeout(ctx, *config.Get().WebHookTimeout) defer cancel() @@ -38,11 +39,23 @@ func SendWebHook(ctx context.Context, obj template.MessageType) error { return nil } - webhookBody, err := template.Message(template.MessageType{ - Node: obj.Node, - Event: obj.Event, - Template: *config.Get().WebHookTemplate, - }) + message, err := template.NewMessageType(ctx, obj.NodeName, obj.Event) + if err != nil { + return errors.Wrap(err, "error in template.NewMessageType") + } + + message.Template = *config.Get().WebHookTemplate + + if len(*config.Get().WebHookTemplateFile) > 0 { + templateFile, err := os.ReadFile(*config.Get().WebHookTemplateFile) + if err != nil { + return errors.Wrap(err, "error in os.ReadFile") + } + + message.Template = string(templateFile) + } + + webhookBody, err := template.Message(message) if err != nil { return errors.Wrap(err, "error in template.Message") } diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go index 74d03cc..e6df61a 100644 --- a/pkg/webhook/webhook_test.go +++ b/pkg/webhook/webhook_test.go @@ -72,14 +72,14 @@ func TestWebHook(t *testing.T) { //nolint:funlen,tparallel Name: "ValidHookAndTemplate", Args: map[string]string{ "webhook.url": getWebhookURL(), - "webhook.template": `node_termination_event{node="{{ .Node }}"} 1`, + "webhook.template": `node_termination_event{node="{{ .NodeName }}"} 1`, }, }, { Name: "EmptyURL", Args: map[string]string{ "webhook.url": "", - "webhook.template": `node_termination_event{node="{{ .Node }}"} 1`, + "webhook.template": `node_termination_event{node="{{ .NodeName }}"} 1`, }, }, { @@ -94,7 +94,7 @@ func TestWebHook(t *testing.T) { //nolint:funlen,tparallel Name: "InvalidContext", Args: map[string]string{ "webhook.url": "example.com", - "webhook.template": `{{ .Node }}`, + "webhook.template": `{{ .NodeName }}`, }, Error: true, }, @@ -102,7 +102,7 @@ func TestWebHook(t *testing.T) { //nolint:funlen,tparallel Name: "InvalidStatus", Args: map[string]string{ "webhook.url": ts.URL, - "webhook.template": `{{ .Node }}`, + "webhook.template": `{{ .NodeName }}`, }, Error: true, }, @@ -110,22 +110,48 @@ func TestWebHook(t *testing.T) { //nolint:funlen,tparallel Name: "InvalidMethod", Args: map[string]string{ "webhook.url": getWebhookURL(), - "webhook.template": `{{ .Node }}`, + "webhook.template": `{{ .NodeName }}`, "webhook.method": `???`, }, Error: true, }, + { + Name: "WebhookTemplateFile", + Args: map[string]string{ + "webhook.url": getWebhookURL(), + "webhook.template-file": "testdata/WebhookTemplateFile.txt", + }, + }, + { + Error: true, + Name: "WebhookTemplateFileInvalid", + Args: map[string]string{ + "webhook.url": getWebhookURL(), + "webhook.template-file": "faketestdata/WebhookTemplateFile.txt", + }, + }, + } + + // clear flags + cleanAllFlags := func() { + for _, test := range tests { + for key := range test.Args { + _ = flag.Set(key, "") + } + } } for _, test := range tests { //nolint:paralleltest tc := test t.Run(test.Name, func(t *testing.T) { + cleanAllFlags() + for key, value := range tc.Args { _ = flag.Set(key, value) } - err := webhook.SendWebHook(context.Background(), template.MessageType{ - Node: "test", + err := webhook.SendWebHook(context.TODO(), &template.MessageType{ + NodeName: "test", }) if tc.Error { require.Error(t, err)