diff --git a/api/checkly/v1alpha1/alertchannel_types.go b/api/checkly/v1alpha1/alertchannel_types.go index b4f7469..69cd5cb 100644 --- a/api/checkly/v1alpha1/alertchannel_types.go +++ b/api/checkly/v1alpha1/alertchannel_types.go @@ -28,16 +28,28 @@ type AlertChannelSpec struct { // Important: Run "make" to regenerate code after modifying this file // SendRecovery determines if the Recovery event should be sent to the alert channel - SendRecovery bool `json:"sendrecovery,omitempty"` + SendRecovery bool `json:"sendRecovery,omitempty"` // SendFailure determines if the Failure event should be sent to the alerting channel - SendFailure bool `json:"sendfailure,omitempty"` + SendFailure bool `json:"sendFailure,omitempty"` + + // SendDegraded determines if the Failure event should be sent to the alerting channel + SendDegraded bool `json:"sendDegraded,omitempty"` + + // SSLExpiry determine if alerts on SSL Expiry should be sent + SSLExpiry bool `json:"sslExpiry,omitempty"` + + // SSLExpiryThreshold At what moment in time to start alerting on SSL certificates. + SSLExpiryThreshold int `json:"sslExpiryThreshold,omitempty"` // OpsGenie holds information about the Opsgenie alert configuration OpsGenie AlertChannelOpsGenie `json:"opsgenie,omitempty"` // Email holds information about the Email alert configuration Email checkly.AlertChannelEmail `json:"email,omitempty"` + + // Webhook holds information about the Webhook alert configuration + Webhook AlertChannelWebhook `json:"webhook,omitempty"` } type AlertChannelOpsGenie struct { @@ -51,6 +63,18 @@ type AlertChannelOpsGenie struct { Priority string `json:"priority,omitempty"` } +// AlertChannelWebhook is a custom struct to hold information about Webhook configuration, source https://github.com/checkly/checkly-go-sdk/blame/main/types.go#L799-L808 +type AlertChannelWebhook struct { + Name string `json:"name"` + URL string `json:"url"` + WebhookType string `json:"webhookType,omitempty"` + Method string `json:"method"` + Template string `json:"template,omitempty"` + WebhookSecret corev1.ObjectReference `json:"webhookSecret,omitempty"` + Headers []checkly.KeyValue `json:"headers,omitempty"` + QueryParameters []checkly.KeyValue `json:"queryParameters,omitempty"` +} + // AlertChannelStatus defines the observed state of AlertChannel type AlertChannelStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster diff --git a/api/checkly/v1alpha1/zz_generated.deepcopy.go b/api/checkly/v1alpha1/zz_generated.deepcopy.go index fa4bcf5..58764c5 100644 --- a/api/checkly/v1alpha1/zz_generated.deepcopy.go +++ b/api/checkly/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + checkly_go_sdk "github.com/checkly/checkly-go-sdk" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -29,7 +30,7 @@ func (in *AlertChannel) DeepCopyInto(out *AlertChannel) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -104,6 +105,7 @@ func (in *AlertChannelSpec) DeepCopyInto(out *AlertChannelSpec) { *out = *in out.OpsGenie = in.OpsGenie out.Email = in.Email + in.Webhook.DeepCopyInto(&out.Webhook) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannelSpec. @@ -131,6 +133,32 @@ func (in *AlertChannelStatus) DeepCopy() *AlertChannelStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertChannelWebhook) DeepCopyInto(out *AlertChannelWebhook) { + *out = *in + out.WebhookSecret = in.WebhookSecret + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make([]checkly_go_sdk.KeyValue, len(*in)) + copy(*out, *in) + } + if in.QueryParameters != nil { + in, out := &in.QueryParameters, &out.QueryParameters + *out = make([]checkly_go_sdk.KeyValue, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertChannelWebhook. +func (in *AlertChannelWebhook) DeepCopy() *AlertChannelWebhook { + if in == nil { + return nil + } + out := new(AlertChannelWebhook) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApiCheck) DeepCopyInto(out *ApiCheck) { *out = *in diff --git a/config/crd/bases/k8s.checklyhq.com_alertchannels.yaml b/config/crd/bases/k8s.checklyhq.com_alertchannels.yaml index bda9250..813319b 100644 --- a/config/crd/bases/k8s.checklyhq.com_alertchannels.yaml +++ b/config/crd/bases/k8s.checklyhq.com_alertchannels.yaml @@ -105,14 +105,141 @@ spec: required: - apisecret type: object - sendfailure: + sendDegraded: + description: SendDegraded determines if the Failure event should be + sent to the alerting channel + type: boolean + sendFailure: description: SendFailure determines if the Failure event should be sent to the alerting channel type: boolean - sendrecovery: + sendRecovery: description: SendRecovery determines if the Recovery event should be sent to the alert channel type: boolean + sslExpiry: + description: SSLExpiry determine if alerts on SSL Expiry should be + sent + type: boolean + sslExpiryThreshold: + description: SSLExpiryThreshold At what moment in time to start alerting + on SSL certificates. + type: integer + webhook: + description: Webhook holds information about the Webhook alert configuration + properties: + headers: + items: + description: |- + KeyValue represents a key-value pair, for example a request header setting, + or a query parameter. + properties: + key: + type: string + locked: + type: boolean + value: + type: string + required: + - key + - locked + - value + type: object + type: array + method: + type: string + name: + type: string + queryParameters: + items: + description: |- + KeyValue represents a key-value pair, for example a request header setting, + or a query parameter. + properties: + key: + type: string + locked: + type: boolean + value: + type: string + required: + - key + - locked + - value + type: object + type: array + template: + type: string + url: + type: string + webhookSecret: + description: |- + ObjectReference contains enough information to let you inspect or modify the referred object. + --- + New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. + 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. + 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular + restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". + Those cannot be well described when embedded. + 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. + 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity + during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple + and the version of the actual struct is irrelevant. + 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type + will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control. + + + Instead of using this type, create a locally provided and used type that is well-focused on your reference. + For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 . + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + webhookType: + type: string + required: + - method + - name + - url + type: object type: object status: description: AlertChannelStatus defines the observed state of AlertChannel diff --git a/config/samples/checkly_v1alpha1_alertchannel.yaml b/config/samples/checkly_v1alpha1_alertchannel.yaml index 91540ec..b4a7369 100644 --- a/config/samples/checkly_v1alpha1_alertchannel.yaml +++ b/config/samples/checkly_v1alpha1_alertchannel.yaml @@ -3,6 +3,11 @@ kind: AlertChannel metadata: name: alertchannel-sample spec: + sendRecovery: false + sendFailure: true + sendDegraded: true + sslExpiry: true + sslExpiryThreshold: 30 # only one of the below can be specified at once, either email or opsgenie email: address: "foo@bar.baz" @@ -13,3 +18,24 @@ spec: # fieldPath: "TEST" # Key inside the secret # priority: "P3" # region: "US" + # sslExpiry: true + # sendDegraded: true + # sslExpiryThreshold: 15 + # webhook: + # name: foo # Name of the webhook + # url: https://foo.bar # URL of the webhook + # webhookType : baz # Type of webhook + # method: POST # Method of webhook + # template: testing # Checkly webhook template + # webhookSecret: + # name: test-secret # Name of the secret or configmap which holds the webhook secret + # namespace: default # Namespace of the secret or configmap + # fieldPath: "SECRET_KEY" # Key inside the secret or configmap + # headers: + # - key: "foo" + # value: "bar" + # locked: true + # queryParameters: + # - key: "bar" + # value: "baz" + # locked: false diff --git a/docs/alert-channels.md b/docs/alert-channels.md index 155d205..bf514d8 100644 --- a/docs/alert-channels.md +++ b/docs/alert-channels.md @@ -6,7 +6,36 @@ See the [official checkly docs](https://www.checklyhq.com/docs/alerting/) on wha The name of the Alert channel derives from the `metadata.name` of the created kubernetes resource. -We're supporting the email and OpsGenie configurations. You can not specify both in a config as each alert channel can only have one channel, if you want to alert to multiple channels, create a resource for each and later reference them in the check group configuration. +We're supporting the email, OpsGenie and webhook configurations. You can not specify all in a config as each alert channel can only have one channel, if you want to alert to multiple channels, create a resource for each and later reference them in the check group configuration. + +### Spec + +| Option | Details | Default | +|--------------|-----------|------------| +| `sendRecovery` | Bool; Should recovery alerts be sent | none | +| `sendFailure` | Bool; Should failure alerts be sent | none | +| `sslExpiry` | Bool; Should ssl expiry check alerts be sent | none | +| `sendDegraded` | Bool; Should degraded alerts be sent | none | +| `sslExpiryThreshold` | int; At what moment in time to start alerting on SSL certificates. | none | +| `email.address` | string; Which email address should the alert be sent to | none | +| `opsgenie.apikey.name` | string; Name of the secret or configmap which holds the Opsgenie API key | none | +| `opsgenie.apikey.namespace` | string; Namespace of the secret or configmap | none | +| `opsgenie.apikey.fieldPath` | string; Key inside the secret or configmap | none | +| `webhook.name` | string; Name for the webhook | none | +| `webhook.url` | string; URL for the webhook | none | +| `webhook.webhookType` | string; TODO: can't determine what this is | none | +| `webhook.method` | string; HTTP type for the webhook (POST/GET/PUT/HEAD/DELETE/PATCH) | none | +| `webhook.template` | string; Template for webhook message | none | +| `webhook.name` | string; Name for the webhook | none | +| `webhook.webhookSecret.name` | string; Name of the secret or configmap which holds the Opsgenie API key | none | +| `webhook.webhookSecret.namespace` | string; Namespace of the secret or configmap | none | +| `webhook.webhookSecret.fieldPath` | string; Key inside the secret or configmap | none | +| `webhook.(*).headers.key` | string; Name for the header key | none | +| `webhook.(*).headers.value` | string; Value of the header | none | +| `webhook.(*).headers.locked` | bool; Is the header value visible in the checklyhq console | none | +| `webhook.(*).queryParameters.key` | string; Name for the query parameter key | none | +| `webhook.(*).queryParameters.value` | string; Value of the query parameter | none | +| `webhook.(*).queryParameters.locked` | bool; Is the query parameter value visible in the checklyhq console | none | ### Email @@ -18,6 +47,10 @@ kind: AlertChannel metadata: name: checkly-operator-test-email spec: + sendRecovery: false + sendFailure: true + sslExpiry: true + sslExpiryThreshold: 30 email: address: "foo@bar.baz" ``` @@ -35,6 +68,10 @@ kind: AlertChannel metadata: name: checkly-operator-test-opsgenie spec: + sendRecovery: false + sendFailure: true + sslExpiry: true + sslExpiryThreshold: 30 opsgenie: apikey: name: test-secret # Name of the secret or configmap which holds the API key @@ -44,6 +81,46 @@ spec: region: "EU" # Your OpsGenie region ``` +### Webhook +The webhook integration supports all the fields which are supported by the checkly-go-sdk, see [details](https://pkg.go.dev/github.com/checkly/checkly-go-sdk#AlertChannelWebhook). For other fields and their options, please see [the official docs](https://www.checklyhq.com/docs/alerting-and-retries/webhooks/). + +The `WebhookSecret` is an optional field and it requires a kubernetes secret (just like the OpsGenie integration). + +Minimum required fields: +* `name` - string +* `url` - string +* `method` - string + +```yaml +apiVersion: k8s.checklyhq.com/v1alpha1 +kind: AlertChannel +metadata: + name: checkly-operator-test-webhook +spec: + sendRecovery: false + sendFailure: true + sslExpiry: true + sslExpiryThreshold: 30 + webhook: + name: foo # Name of the webhook + url: https://foo.bar # URL of the webhook + webhookType : baz # Type of webhook + method: POST # Method of webhook + template: testing # Checkly webhook template + webhookSecret: + name: test-secret # Name of the secret or configmap which holds the webhook secret + namespace: default # Namespace of the secret or configmap + fieldPath: "SECRET_KEY" # Key inside the secret or configmap + headers: + - key: "foo" + value: "bar" + locked: true # Not visible in the UI + queryParameters: + - key: "bar" + value: "baz" + locked: false # Visible in the UI +``` + ## Referencing You'll need to reference the name of the alert channel in the group check configuration. See [check-group](check-group.md) for more details. diff --git a/external/checkly/alertChannel.go b/external/checkly/alertChannel.go index bd74e03..c8a202b 100644 --- a/external/checkly/alertChannel.go +++ b/external/checkly/alertChannel.go @@ -8,13 +8,16 @@ import ( checklyv1alpha1 "github.com/checkly/checkly-operator/api/checkly/v1alpha1" ) -func checklyAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie) (ac checkly.AlertChannel, err error) { - sslExpiry := false - +func checklyAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, webhookConfig checkly.AlertChannelWebhook) (ac checkly.AlertChannel, err error) { ac = checkly.AlertChannel{ SendRecovery: &alertChannel.Spec.SendRecovery, SendFailure: &alertChannel.Spec.SendFailure, - SSLExpiry: &sslExpiry, + SendDegraded: &alertChannel.Spec.SendDegraded, + SSLExpiry: &alertChannel.Spec.SSLExpiry, + } + + if (alertChannel.Spec.SSLExpiryThreshold > 0) && (alertChannel.Spec.SSLExpiryThreshold < 30) { + ac.SSLExpiryThreshold = &alertChannel.Spec.SSLExpiryThreshold } if opsGenieConfig != (checkly.AlertChannelOpsgenie{}) { @@ -30,12 +33,17 @@ func checklyAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieCon } return } + + if webhookConfig.Name != "" { // Struct has []KeyValue types which can't be compared + ac.Type = "WEBHOOK" // Type has to be all caps, see https://developers.checklyhq.com/reference/postv1alertchannels + ac.Webhook = &webhookConfig + } return } -func CreateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, client checkly.Client) (ID int64, err error) { +func CreateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, webhookConfig checkly.AlertChannelWebhook, client checkly.Client) (ID int64, err error) { - ac, err := checklyAlertChannel(alertChannel, opsGenieConfig) + ac, err := checklyAlertChannel(alertChannel, opsGenieConfig, webhookConfig) if err != nil { return } @@ -53,8 +61,8 @@ func CreateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConf return } -func UpdateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, client checkly.Client) (err error) { - ac, err := checklyAlertChannel(alertChannel, opsGenieConfig) +func UpdateAlertChannel(alertChannel *checklyv1alpha1.AlertChannel, opsGenieConfig checkly.AlertChannelOpsgenie, webhookConfig checkly.AlertChannelWebhook, client checkly.Client) (err error) { + ac, err := checklyAlertChannel(alertChannel, opsGenieConfig, webhookConfig) if err != nil { return } diff --git a/external/checkly/alertChannel_test.go b/external/checkly/alertChannel_test.go index 3fccf0f..9f0a218 100644 --- a/external/checkly/alertChannel_test.go +++ b/external/checkly/alertChannel_test.go @@ -27,8 +27,9 @@ func TestChecklyAlertChannel(t *testing.T) { } opsGenieConfigEmpty := checkly.AlertChannelOpsgenie{} + webhookConfigEmpty := checkly.AlertChannelWebhook{} - returned, err := checklyAlertChannel(&dataEmpty, opsGenieConfigEmpty) + returned, err := checklyAlertChannel(&dataEmpty, opsGenieConfigEmpty, webhookConfigEmpty) if err != nil { t.Errorf("Expected no error, got %e", err) } @@ -42,7 +43,7 @@ func TestChecklyAlertChannel(t *testing.T) { Address: acEmailAddress, } - returned, err = checklyAlertChannel(&dataEmail, opsGenieConfigEmpty) + returned, err = checklyAlertChannel(&dataEmail, opsGenieConfigEmpty, webhookConfigEmpty) if err != nil { t.Errorf("Expected no error, got %e", err) } @@ -58,7 +59,7 @@ func TestChecklyAlertChannel(t *testing.T) { Name: "baz", } - returned, err = checklyAlertChannel(&dataEmpty, dataOpsGenieFull) + returned, err = checklyAlertChannel(&dataEmpty, dataOpsGenieFull, webhookConfigEmpty) if err != nil { t.Errorf("Expected no error, got %e", err) } @@ -79,6 +80,42 @@ func TestChecklyAlertChannel(t *testing.T) { t.Errorf("Expected nil, got %s", returned.Email) } + if returned.Webhook != nil { // Can't test against nil because []KeyValue pairs are present + t.Errorf("Expected nil, got %v+", returned.Webhook) + } + + dataWebhookFull := checkly.AlertChannelWebhook{ + Name: "test", + URL: "http://foo.bar", + WebhookType: "GET", + Method: "POST", + Template: "", + WebhookSecret: "foobar", + Headers: []checkly.KeyValue{}, + QueryParameters: []checkly.KeyValue{}, + } + + returned, err = checklyAlertChannel(&dataEmpty, opsGenieConfigEmpty, dataWebhookFull) + if err != nil { + t.Errorf("Expected error, got %e", err) + } + + if returned.Webhook == nil { + t.Errorf("Expected Webhook field to be populated, it's empty") + } + + if returned.Webhook.Method != "POST" { + t.Errorf("Expected %s, got %s", "POST", returned.Webhook.Method) + } + + if returned.Opsgenie != nil { + t.Errorf("Expected nil, got %s", returned.Opsgenie) + } + + if returned.Email != nil { + t.Errorf("Expected nil, got %s", returned.Email) + } + } func TestAlertChannelActions(t *testing.T) { @@ -101,6 +138,7 @@ func TestAlertChannelActions(t *testing.T) { } opsGenieConfigEmpty := checkly.AlertChannelOpsgenie{} + webhookConfigEmpty := checkly.AlertChannelWebhook{} // Test errors testClient := checkly.NewClient( @@ -112,13 +150,13 @@ func TestAlertChannelActions(t *testing.T) { testClient.SetAccountId("1234567890") // Create fail - _, err := CreateAlertChannel(testData, opsGenieConfigEmpty, testClient) + _, err := CreateAlertChannel(testData, opsGenieConfigEmpty, webhookConfigEmpty, testClient) if err == nil { t.Error("Expected error, got none") } // Update fail - err = UpdateAlertChannel(testData, opsGenieConfigEmpty, testClient) + err = UpdateAlertChannel(testData, opsGenieConfigEmpty, webhookConfigEmpty, testClient) if err == nil { t.Error("Expected error, got none") } @@ -159,7 +197,7 @@ func TestAlertChannelActions(t *testing.T) { }() // Create success - testID, err := CreateAlertChannel(testData, opsGenieConfigEmpty, testClient) + testID, err := CreateAlertChannel(testData, opsGenieConfigEmpty, webhookConfigEmpty, testClient) if err != nil { t.Errorf("Expected no error, got %e", err) } @@ -168,7 +206,7 @@ func TestAlertChannelActions(t *testing.T) { } // Update success - err = UpdateAlertChannel(testData, opsGenieConfigEmpty, testClient) + err = UpdateAlertChannel(testData, opsGenieConfigEmpty, webhookConfigEmpty, testClient) if err != nil { t.Errorf("Expected no error, got %e", err) } diff --git a/internal/controller/checkly/alertchannel_controller.go b/internal/controller/checkly/alertchannel_controller.go index 22105e7..84b7c2c 100644 --- a/internal/controller/checkly/alertchannel_controller.go +++ b/internal/controller/checkly/alertchannel_controller.go @@ -23,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -84,17 +85,22 @@ func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request if ac.GetDeletionTimestamp() != nil { if controllerutil.ContainsFinalizer(ac, acFinalizer) { logger.Info("Finalizer is present, trying to delete Checkly AlertChannel", "ID", ac.Status.ID) - err := external.DeleteAlertChannel(ac, r.ApiClient) - if err != nil { - logger.Error(err, "Failed to delete checkly AlertChannel") - return ctrl.Result{}, err + if ac.Status.ID != 0 { + err := external.DeleteAlertChannel(ac, r.ApiClient) + if err != nil { + logger.Error(err, "Failed to delete checkly AlertChannel") + return ctrl.Result{}, err + } + logger.Info("Successfully deleted checkly AlertChannel", "ID", ac.Status.ID) + + } else { + logger.Info("Alertchannel was not created on checklyhq.com, won't delete it upstream.") } - logger.Info("Successfully deleted checkly AlertChannel", "ID", ac.Status.ID) - controllerutil.RemoveFinalizer(ac, acFinalizer) err = r.Update(ctx, ac) if err != nil { + logger.Error(err, "Failed to remove finalizer") return ctrl.Result{}, err } logger.Info("Successfully deleted finalizer from AlertChannel") @@ -113,7 +119,7 @@ func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } logger.Info("Added finalizer", "checkly AlertChannel ID", ac.Status.ID) - return ctrl.Result{}, nil + return ctrl.Result{Requeue: true}, nil } // ///////////////////////////// @@ -121,20 +127,9 @@ func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request // //////////////////////////// opsGenieConfig := checkly.AlertChannelOpsgenie{} if ac.Spec.OpsGenie.APISecret != (corev1.ObjectReference{}) { - secret := &corev1.Secret{} - err := r.Get(ctx, - types.NamespacedName{ - Name: ac.Spec.OpsGenie.APISecret.Name, - Namespace: ac.Spec.OpsGenie.APISecret.Namespace}, - secret) + secretValue, err := r.GetSecretValue(ctx, ac.Spec.OpsGenie.APISecret) if err != nil { - logger.Info("Unable to read secret for API Key", "err", err) - return ctrl.Result{}, err - } - - secretValue := string(secret.Data[ac.Spec.OpsGenie.APISecret.FieldPath]) - if secretValue == "" { - logger.Info("Secret value is empty") + logger.Error(err, "couldn't retrieve secret value") return ctrl.Result{}, err } @@ -147,6 +142,32 @@ func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request } + // ///////////////////////////// + // Webhook logic + secret retrieval + // //////////////////////////// + + var webhookConfig checkly.AlertChannelWebhook + var webhookSecretValue string + if ac.Spec.Webhook.WebhookSecret != (corev1.ObjectReference{}) { + webhookSecretValue, err = r.GetSecretValue(ctx, ac.Spec.Webhook.WebhookSecret) + if err != nil { + logger.Error(err, "couldn't retrieve secret value") + return ctrl.Result{}, err + } + + } + + webhookConfig = checkly.AlertChannelWebhook{ + Name: ac.Spec.Webhook.Name, + URL: ac.Spec.Webhook.URL, + WebhookType: ac.Spec.Webhook.WebhookType, + Method: ac.Spec.Webhook.Method, + Template: ac.Spec.Webhook.Template, + WebhookSecret: webhookSecretValue, + Headers: ac.Spec.Webhook.Headers, + QueryParameters: ac.Spec.Webhook.QueryParameters, + } + // ///////////////////////////// // Update logic // //////////////////////////// @@ -155,7 +176,7 @@ func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request if ac.Status.ID != 0 { // Existing object, we need to update it logger.Info("Existing object, with ID", "checkly AlertChannel ID", ac.Status.ID) - err := external.UpdateAlertChannel(ac, opsGenieConfig, r.ApiClient) + err := external.UpdateAlertChannel(ac, opsGenieConfig, webhookConfig, r.ApiClient) if err != nil { logger.Error(err, "Failed to update checkly AlertChannel") return ctrl.Result{}, err @@ -167,7 +188,7 @@ func (r *AlertChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request // ///////////////////////////// // Create logic // //////////////////////////// - acID, err := external.CreateAlertChannel(ac, opsGenieConfig, r.ApiClient) + acID, err := external.CreateAlertChannel(ac, opsGenieConfig, webhookConfig, r.ApiClient) if err != nil { logger.Error(err, "Failed to create checkly AlertChannel") return ctrl.Result{}, err @@ -191,3 +212,24 @@ func (r *AlertChannelReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&checklyv1alpha1.AlertChannel{}). Complete(r) } + +func (r *AlertChannelReconciler) GetSecretValue(ctx context.Context, secretObject corev1.ObjectReference) (secretValue string, err error) { + secret := &corev1.Secret{} + err = r.Get(ctx, + types.NamespacedName{ + Name: secretObject.Name, + Namespace: secretObject.Namespace, + }, secret) + + if err != nil { + return + } + + secretValue = string(secret.Data[secretObject.FieldPath]) + if secretValue == "" { + err = errors.NewNotFound(schema.GroupResource{Group: "corev1", Resource: "secret"}, secretObject.Name) + return + } + + return +} diff --git a/internal/controller/checkly/alertchannel_controller_test.go b/internal/controller/checkly/alertchannel_controller_test.go index 1c96909..2502941 100644 --- a/internal/controller/checkly/alertchannel_controller_test.go +++ b/internal/controller/checkly/alertchannel_controller_test.go @@ -195,5 +195,76 @@ var _ = Describe("ApiCheck Controller", func() { }, timeout, interval).Should(Succeed()) }) // return + + It("Test failures", func() { + acFailKey := types.NamespacedName{ + Name: "test-alert-channel-failure", + } + + secretKey := types.NamespacedName{ + Name: "test-secret", + Namespace: "default", + } + + // secretData := map[string][]byte{ + // "TEST": []byte("test"), + // } + + // secret := &corev1.Secret{ + // ObjectMeta: metav1.ObjectMeta{ + // Name: secretKey.Name, + // Namespace: secretKey.Namespace, + // }, + // Data: secretData, + // } + + alertChannel := &checklyv1alpha1.AlertChannel{ + ObjectMeta: metav1.ObjectMeta{ + Name: acFailKey.Name, + }, + Spec: checklyv1alpha1.AlertChannelSpec{ + SendFailure: false, + Webhook: checklyv1alpha1.AlertChannelWebhook{ + Name: "test-failure", + URL: "http://foo.bar", + Method: "POST", + WebhookSecret: corev1.ObjectReference{ + Namespace: secretKey.Namespace, + Name: secretKey.Name, + FieldPath: "TEST", + }, + }, + }, + } + + Expect(k8sClient.Create(context.Background(), alertChannel)).Should(Succeed()) + + By("No ID present") + Eventually(func() bool { + f := &checklyv1alpha1.AlertChannel{} + err := k8sClient.Get(context.Background(), acFailKey, f) + if err != nil { + return false + } + + Expect(f.Status.ID).Should(Equal(int64(0)), "Expecting empty value") + + return true + }, timeout, interval).Should(BeTrue()) + + // Delete AlertChannel + By("Expecting to delete alertchannel successfully") + Eventually(func() error { + f := &checklyv1alpha1.AlertChannel{} + k8sClient.Get(context.Background(), acFailKey, f) + return k8sClient.Delete(context.Background(), f) + }, timeout, interval).Should(Succeed()) + + By("Expecting delete to finish") + Eventually(func() error { + f := &checklyv1alpha1.AlertChannel{} + return k8sClient.Get(context.Background(), acFailKey, f) + }, timeout, interval).ShouldNot(Succeed()) + }) }) })