diff --git a/Makefile b/Makefile index 6ab7a07e..a0be1714 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ endif eval \$$($(GOBIN)/setup-envtest use -p env ${TEST_K8S_VERSION}); \ export USE_CERTMANAGER=false; \ export TEST_USE_EXISTING_CLUSTER=false; \ - $(GINKGO) -vv --procs 3 -output-dir=${PWD} --output-interceptor-mode=none -keep-separate-reports --junit-report=test-results-junit.xml --randomize-suites --randomize-all -timeout 10m ./... -covermode=count -coverprofile cover.out \ + $(GINKGO) -vv --no-color --procs 3 -output-dir=${PWD} --output-interceptor-mode=none -keep-separate-reports --junit-report=test-results-junit.xml --randomize-suites --randomize-all -timeout 10m ./... -covermode=count -coverprofile cover.out \ " ##@ Build diff --git a/api/v1alpha1/humioaction_types.go b/api/v1alpha1/humioaction_types.go index 88ef4ed7..d2669ff0 100644 --- a/api/v1alpha1/humioaction_types.go +++ b/api/v1alpha1/humioaction_types.go @@ -34,12 +34,30 @@ const ( // HumioActionWebhookProperties defines the desired state of HumioActionWebhookProperties type HumioActionWebhookProperties struct { - BodyTemplate string `json:"bodyTemplate,omitempty"` - Headers map[string]string `json:"headers,omitempty"` - Method string `json:"method,omitempty"` - Url string `json:"url,omitempty"` - IgnoreSSL bool `json:"ignoreSSL,omitempty"` - UseProxy bool `json:"useProxy,omitempty"` + BodyTemplate string `json:"bodyTemplate,omitempty"` + // Headers specifies what HTTP headers to use. + // If both Headers and SecretHeaders are specified, they will be merged together. + Headers map[string]string `json:"headers,omitempty"` + // SecretHeaders specifies what HTTP headers to use and where to fetch the values from. + // If both Headers and SecretHeaders are specified, they will be merged together. + SecretHeaders []HeadersSource `json:"secretHeaders,omitempty"` + Method string `json:"method,omitempty"` + // Url specifies what URL to use + // If both Url and UrlSource are specified, Url will be used. + Url string `json:"url,omitempty"` + // UrlSource specifies where to fetch the URL from + // If both Url and UrlSource are specified, Url will be used. + UrlSource VarSource `json:"urlSource,omitempty"` + IgnoreSSL bool `json:"ignoreSSL,omitempty"` + UseProxy bool `json:"useProxy,omitempty"` +} + +// HeadersSource defines a header and corresponding source for the value of it. +type HeadersSource struct { + // Name is the name of the header. + Name string `json:"name,omitempty"` + // ValueFrom defines where to fetch the value of the header from. + ValueFrom VarSource `json:"valueFrom,omitempty"` } // HumioActionEmailProperties defines the desired state of HumioActionEmailProperties @@ -52,50 +70,79 @@ type HumioActionEmailProperties struct { // HumioActionRepositoryProperties defines the desired state of HumioActionRepositoryProperties type HumioActionRepositoryProperties struct { - IngestToken string `json:"ingestToken,omitempty"` + // IngestToken specifies what ingest token to use. + // If both IngestToken and IngestTokenSource are specified, IngestToken will be used. + IngestToken string `json:"ingestToken,omitempty"` + // IngestTokenSource specifies where to fetch the ingest token from. + // If both IngestToken and IngestTokenSource are specified, IngestToken will be used. IngestTokenSource VarSource `json:"ingestTokenSource,omitempty"` } // HumioActionOpsGenieProperties defines the desired state of HumioActionOpsGenieProperties type HumioActionOpsGenieProperties struct { - ApiUrl string `json:"apiUrl,omitempty"` - GenieKey string `json:"genieKey,omitempty"` + ApiUrl string `json:"apiUrl,omitempty"` + // GenieKey specifies what API key to use. + // If both GenieKey and GenieKeySource are specified, GenieKey will be used. + GenieKey string `json:"genieKey,omitempty"` + // GenieKeySource specifies where to fetch the API key from. + // If both GenieKey and GenieKeySource are specified, GenieKey will be used. GenieKeySource VarSource `json:"genieKeySource,omitempty"` UseProxy bool `json:"useProxy,omitempty"` } // HumioActionPagerDutyProperties defines the desired state of HumioActionPagerDutyProperties type HumioActionPagerDutyProperties struct { + // RoutingKey specifies what API key to use. + // If both RoutingKey and RoutingKeySource are specified, RoutingKey will be used. RoutingKey string `json:"routingKey,omitempty"` - Severity string `json:"severity,omitempty"` - UseProxy bool `json:"useProxy,omitempty"` + // RoutingKeySource specifies where to fetch the routing key from. + // If both RoutingKey and RoutingKeySource are specified, RoutingKey will be used. + RoutingKeySource VarSource `json:"routingKeySource,omitempty"` + Severity string `json:"severity,omitempty"` + UseProxy bool `json:"useProxy,omitempty"` } // HumioActionSlackProperties defines the desired state of HumioActionSlackProperties type HumioActionSlackProperties struct { - Fields map[string]string `json:"fields,omitempty"` - Url string `json:"url,omitempty"` - UseProxy bool `json:"useProxy,omitempty"` + Fields map[string]string `json:"fields,omitempty"` + // Url specifies what URL to use. + // If both Url and UrlSource are specified, Url will be used. + Url string `json:"url,omitempty"` + // UrlSource specifies where to fetch the URL from. + // If both Url and UrlSource are specified, Url will be used. + UrlSource VarSource `json:"urlSource,omitempty"` + UseProxy bool `json:"useProxy,omitempty"` } // HumioActionSlackPostMessageProperties defines the desired state of HumioActionSlackPostMessageProperties type HumioActionSlackPostMessageProperties struct { - ApiToken string `json:"apiToken,omitempty"` + // ApiToken specifies what API key to use. + // If both ApiToken and ApiTokenSource are specified, ApiToken will be used. + ApiToken string `json:"apiToken,omitempty"` + // ApiTokenSource specifies where to fetch the API key from. + // If both ApiToken and ApiTokenSource are specified, ApiToken will be used. ApiTokenSource VarSource `json:"apiTokenSource,omitempty"` Channels []string `json:"channels,omitempty"` Fields map[string]string `json:"fields,omitempty"` UseProxy bool `json:"useProxy,omitempty"` } -type VarSource struct { - SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` -} - // HumioActionVictorOpsProperties defines the desired state of HumioActionVictorOpsProperties type HumioActionVictorOpsProperties struct { MessageType string `json:"messageType,omitempty"` - NotifyUrl string `json:"notifyUrl,omitempty"` - UseProxy bool `json:"useProxy,omitempty"` + // NotifyUrl specifies what URL to use. + // If both NotifyUrl and NotifyUrlSource are specified, NotifyUrl will be used. + NotifyUrl string `json:"notifyUrl,omitempty"` + // NotifyUrlSource specifies where to fetch the URL from. + // If both NotifyUrl and NotifyUrlSource are specified, NotifyUrl will be used. + NotifyUrlSource VarSource `json:"notifyUrlSource"` + UseProxy bool `json:"useProxy,omitempty"` +} + +// VarSource is used to specify where a value should be pulled from +type VarSource struct { + // SecretKeyRef allows specifying which secret and what key in that secret holds the value we want to use + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` } // HumioActionSpec defines the desired state of HumioAction diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1d07634c..54adc166 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HeadersSource) DeepCopyInto(out *HeadersSource) { + *out = *in + in.ValueFrom.DeepCopyInto(&out.ValueFrom) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeadersSource. +func (in *HeadersSource) DeepCopy() *HeadersSource { + if in == nil { + return nil + } + out := new(HeadersSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioAction) DeepCopyInto(out *HumioAction) { *out = *in @@ -123,6 +139,7 @@ func (in *HumioActionOpsGenieProperties) DeepCopy() *HumioActionOpsGenieProperti // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioActionPagerDutyProperties) DeepCopyInto(out *HumioActionPagerDutyProperties) { *out = *in + in.RoutingKeySource.DeepCopyInto(&out.RoutingKeySource) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioActionPagerDutyProperties. @@ -189,6 +206,7 @@ func (in *HumioActionSlackProperties) DeepCopyInto(out *HumioActionSlackProperti (*out)[key] = val } } + in.UrlSource.DeepCopyInto(&out.UrlSource) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioActionSlackProperties. @@ -222,7 +240,7 @@ func (in *HumioActionSpec) DeepCopyInto(out *HumioActionSpec) { if in.PagerDutyProperties != nil { in, out := &in.PagerDutyProperties, &out.PagerDutyProperties *out = new(HumioActionPagerDutyProperties) - **out = **in + (*in).DeepCopyInto(*out) } if in.SlackProperties != nil { in, out := &in.SlackProperties, &out.SlackProperties @@ -237,7 +255,7 @@ func (in *HumioActionSpec) DeepCopyInto(out *HumioActionSpec) { if in.VictorOpsProperties != nil { in, out := &in.VictorOpsProperties, &out.VictorOpsProperties *out = new(HumioActionVictorOpsProperties) - **out = **in + (*in).DeepCopyInto(*out) } if in.WebhookProperties != nil { in, out := &in.WebhookProperties, &out.WebhookProperties @@ -274,6 +292,7 @@ func (in *HumioActionStatus) DeepCopy() *HumioActionStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HumioActionVictorOpsProperties) DeepCopyInto(out *HumioActionVictorOpsProperties) { *out = *in + in.NotifyUrlSource.DeepCopyInto(&out.NotifyUrlSource) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioActionVictorOpsProperties. @@ -296,6 +315,14 @@ func (in *HumioActionWebhookProperties) DeepCopyInto(out *HumioActionWebhookProp (*out)[key] = val } } + if in.SecretHeaders != nil { + in, out := &in.SecretHeaders, &out.SecretHeaders + *out = make([]HeadersSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.UrlSource.DeepCopyInto(&out.UrlSource) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HumioActionWebhookProperties. diff --git a/charts/humio-operator/crds/core.humio.com_humioactions.yaml b/charts/humio-operator/crds/core.humio.com_humioactions.yaml index 5afbcf00..2b39c18c 100644 --- a/charts/humio-operator/crds/core.humio.com_humioactions.yaml +++ b/charts/humio-operator/crds/core.humio.com_humioactions.yaml @@ -70,11 +70,18 @@ spec: Action, and contains the corresponding properties properties: ingestToken: + description: |- + IngestToken specifies what ingest token to use. + If both IngestToken and IngestTokenSource are specified, IngestToken will be used. type: string ingestTokenSource: + description: |- + IngestTokenSource specifies where to fetch the ingest token from. + If both IngestToken and IngestTokenSource are specified, IngestToken will be used. properties: secretKeyRef: - description: SecretKeySelector selects a key of a Secret. + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use properties: key: description: The key of the secret to select from. Must @@ -112,11 +119,18 @@ spec: apiUrl: type: string genieKey: + description: |- + GenieKey specifies what API key to use. + If both GenieKey and GenieKeySource are specified, GenieKey will be used. type: string genieKeySource: + description: |- + GenieKeySource specifies where to fetch the API key from. + If both GenieKey and GenieKeySource are specified, GenieKey will be used. properties: secretKeyRef: - description: SecretKeySelector selects a key of a Secret. + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use properties: key: description: The key of the secret to select from. Must @@ -145,7 +159,38 @@ spec: and contains the corresponding properties properties: routingKey: + description: |- + RoutingKey specifies what API key to use. + If both RoutingKey and RoutingKeySource are specified, RoutingKey will be used. type: string + routingKeySource: + description: |- + RoutingKeySource specifies where to fetch the routing key from. + If both RoutingKey and RoutingKeySource are specified, RoutingKey will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object severity: type: string useProxy: @@ -156,11 +201,18 @@ spec: Post Message Action, and contains the corresponding properties properties: apiToken: + description: |- + ApiToken specifies what API key to use. + If both ApiToken and ApiTokenSource are specified, ApiToken will be used. type: string apiTokenSource: + description: |- + ApiTokenSource specifies where to fetch the API key from. + If both ApiToken and ApiTokenSource are specified, ApiToken will be used. properties: secretKeyRef: - description: SecretKeySelector selects a key of a Secret. + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use properties: key: description: The key of the secret to select from. Must @@ -201,7 +253,38 @@ spec: type: string type: object url: + description: |- + Url specifies what URL to use. + If both Url and UrlSource are specified, Url will be used. type: string + urlSource: + description: |- + UrlSource specifies where to fetch the URL from. + If both Url and UrlSource are specified, Url will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object useProxy: type: boolean type: object @@ -212,9 +295,42 @@ spec: messageType: type: string notifyUrl: + description: |- + NotifyUrl specifies what URL to use. + If both NotifyUrl and NotifyUrlSource are specified, NotifyUrl will be used. type: string + notifyUrlSource: + description: |- + NotifyUrlSource specifies where to fetch the URL from. + If both NotifyUrl and NotifyUrlSource are specified, NotifyUrl will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object useProxy: type: boolean + required: + - notifyUrlSource type: object viewName: description: ViewName is the name of the Humio View under which the @@ -229,13 +345,88 @@ spec: headers: additionalProperties: type: string + description: |- + Headers specifies what HTTP headers to use. + If both Headers and SecretHeaders are specified, they will be merged together. type: object ignoreSSL: type: boolean method: type: string + secretHeaders: + description: |- + SecretHeaders specifies what HTTP headers to use and where to fetch the values from. + If both Headers and SecretHeaders are specified, they will be merged together. + items: + description: HeadersSource defines a header and corresponding + source for the value of it. + properties: + name: + description: Name is the name of the header. + type: string + valueFrom: + description: ValueFrom defines where to fetch the value + of the header from. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret + and what key in that secret holds the value we want + to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + type: array url: + description: |- + Url specifies what URL to use + If both Url and UrlSource are specified, Url will be used. type: string + urlSource: + description: |- + UrlSource specifies where to fetch the URL from + If both Url and UrlSource are specified, Url will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object useProxy: type: boolean type: object diff --git a/config/crd/bases/core.humio.com_humioactions.yaml b/config/crd/bases/core.humio.com_humioactions.yaml index 5afbcf00..2b39c18c 100644 --- a/config/crd/bases/core.humio.com_humioactions.yaml +++ b/config/crd/bases/core.humio.com_humioactions.yaml @@ -70,11 +70,18 @@ spec: Action, and contains the corresponding properties properties: ingestToken: + description: |- + IngestToken specifies what ingest token to use. + If both IngestToken and IngestTokenSource are specified, IngestToken will be used. type: string ingestTokenSource: + description: |- + IngestTokenSource specifies where to fetch the ingest token from. + If both IngestToken and IngestTokenSource are specified, IngestToken will be used. properties: secretKeyRef: - description: SecretKeySelector selects a key of a Secret. + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use properties: key: description: The key of the secret to select from. Must @@ -112,11 +119,18 @@ spec: apiUrl: type: string genieKey: + description: |- + GenieKey specifies what API key to use. + If both GenieKey and GenieKeySource are specified, GenieKey will be used. type: string genieKeySource: + description: |- + GenieKeySource specifies where to fetch the API key from. + If both GenieKey and GenieKeySource are specified, GenieKey will be used. properties: secretKeyRef: - description: SecretKeySelector selects a key of a Secret. + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use properties: key: description: The key of the secret to select from. Must @@ -145,7 +159,38 @@ spec: and contains the corresponding properties properties: routingKey: + description: |- + RoutingKey specifies what API key to use. + If both RoutingKey and RoutingKeySource are specified, RoutingKey will be used. type: string + routingKeySource: + description: |- + RoutingKeySource specifies where to fetch the routing key from. + If both RoutingKey and RoutingKeySource are specified, RoutingKey will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object severity: type: string useProxy: @@ -156,11 +201,18 @@ spec: Post Message Action, and contains the corresponding properties properties: apiToken: + description: |- + ApiToken specifies what API key to use. + If both ApiToken and ApiTokenSource are specified, ApiToken will be used. type: string apiTokenSource: + description: |- + ApiTokenSource specifies where to fetch the API key from. + If both ApiToken and ApiTokenSource are specified, ApiToken will be used. properties: secretKeyRef: - description: SecretKeySelector selects a key of a Secret. + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use properties: key: description: The key of the secret to select from. Must @@ -201,7 +253,38 @@ spec: type: string type: object url: + description: |- + Url specifies what URL to use. + If both Url and UrlSource are specified, Url will be used. type: string + urlSource: + description: |- + UrlSource specifies where to fetch the URL from. + If both Url and UrlSource are specified, Url will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object useProxy: type: boolean type: object @@ -212,9 +295,42 @@ spec: messageType: type: string notifyUrl: + description: |- + NotifyUrl specifies what URL to use. + If both NotifyUrl and NotifyUrlSource are specified, NotifyUrl will be used. type: string + notifyUrlSource: + description: |- + NotifyUrlSource specifies where to fetch the URL from. + If both NotifyUrl and NotifyUrlSource are specified, NotifyUrl will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object useProxy: type: boolean + required: + - notifyUrlSource type: object viewName: description: ViewName is the name of the Humio View under which the @@ -229,13 +345,88 @@ spec: headers: additionalProperties: type: string + description: |- + Headers specifies what HTTP headers to use. + If both Headers and SecretHeaders are specified, they will be merged together. type: object ignoreSSL: type: boolean method: type: string + secretHeaders: + description: |- + SecretHeaders specifies what HTTP headers to use and where to fetch the values from. + If both Headers and SecretHeaders are specified, they will be merged together. + items: + description: HeadersSource defines a header and corresponding + source for the value of it. + properties: + name: + description: Name is the name of the header. + type: string + valueFrom: + description: ValueFrom defines where to fetch the value + of the header from. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret + and what key in that secret holds the value we want + to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + type: array url: + description: |- + Url specifies what URL to use + If both Url and UrlSource are specified, Url will be used. type: string + urlSource: + description: |- + UrlSource specifies where to fetch the URL from + If both Url and UrlSource are specified, Url will be used. + properties: + secretKeyRef: + description: SecretKeyRef allows specifying which secret and + what key in that secret holds the value we want to use + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object useProxy: type: boolean type: object diff --git a/controllers/humioaction_annotations.go b/controllers/humioaction_annotations.go index d3073350..3fd0b198 100644 --- a/controllers/humioaction_annotations.go +++ b/controllers/humioaction_annotations.go @@ -3,11 +3,9 @@ package controllers import ( "context" "fmt" - - "github.com/humio/humio-operator/pkg/humio" - humioapi "github.com/humio/cli/api" humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" + "github.com/humio/humio-operator/pkg/humio" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -20,17 +18,11 @@ func (r *HumioActionReconciler) reconcileHumioActionAnnotations(ctx context.Cont return reconcile.Result{}, r.logErrorAndReturn(err, "failed to add ID annotation to action") } - // Copy annotations from the actions transformer to get the current action ID - action, err := humio.CRActionFromAPIAction(addedAction) - if err != nil { - return reconcile.Result{}, r.logErrorAndReturn(err, "failed to add ID annotation to action") - } if len(actionCR.ObjectMeta.Annotations) < 1 { actionCR.ObjectMeta.Annotations = make(map[string]string) } - for k, v := range action.Annotations { - actionCR.ObjectMeta.Annotations[k] = v - } + + actionCR.ObjectMeta.Annotations[humio.ActionIdentifierAnnotation] = addedAction.ID err = r.Update(ctx, actionCR) if err != nil { diff --git a/controllers/humioaction_controller.go b/controllers/humioaction_controller.go index ae6c43d4..6037e372 100644 --- a/controllers/humioaction_controller.go +++ b/controllers/humioaction_controller.go @@ -181,13 +181,13 @@ func (r *HumioActionReconciler) reconcileHumioAction(ctx context.Context, config sanitizeAction(curAction) sanitizeAction(expectedAction) if !cmp.Equal(*curAction, *expectedAction) { - r.Log.Info("Action differs, triggering update") + r.Log.Info("Action differs, triggering update", "actionDiff", cmp.Diff(*curAction, *expectedAction)) action, err := r.HumioClient.UpdateAction(config, req, ha) if err != nil { return reconcile.Result{}, r.logErrorAndReturn(err, "could not update action") } if action != nil { - r.Log.Info(fmt.Sprintf("Updated action %q", ha.Spec.Name)) + r.Log.Info(fmt.Sprintf("Updated action %q", ha.Spec.Name), "newAction", fmt.Sprintf("%#+v", action)) } } @@ -197,28 +197,74 @@ func (r *HumioActionReconciler) reconcileHumioAction(ctx context.Context, config func (r *HumioActionReconciler) resolveSecrets(ctx context.Context, ha *humiov1alpha1.HumioAction) error { var err error + var apiToken string if ha.Spec.SlackPostMessageProperties != nil { - ha.Spec.SlackPostMessageProperties.ApiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.SlackPostMessageProperties.ApiToken, ha.Spec.SlackPostMessageProperties.ApiTokenSource) + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.SlackPostMessageProperties.ApiToken, ha.Spec.SlackPostMessageProperties.ApiTokenSource) if err != nil { - return fmt.Errorf("slackPostMessageProperties.ingestTokenSource.%v", err) + return fmt.Errorf("slackPostMessageProperties.apiTokenSource.%v", err) } } + if ha.Spec.SlackProperties != nil { + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.SlackProperties.Url, ha.Spec.SlackProperties.UrlSource) + if err != nil { + return fmt.Errorf("slackProperties.urlSource.%v", err) + } + + } + if ha.Spec.OpsGenieProperties != nil { - ha.Spec.OpsGenieProperties.GenieKey, err = r.resolveField(ctx, ha.Namespace, ha.Spec.OpsGenieProperties.GenieKey, ha.Spec.OpsGenieProperties.GenieKeySource) + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.OpsGenieProperties.GenieKey, ha.Spec.OpsGenieProperties.GenieKeySource) if err != nil { - return fmt.Errorf("opsGenieProperties.ingestTokenSource.%v", err) + return fmt.Errorf("opsGenieProperties.genieKeySource.%v", err) } } if ha.Spec.HumioRepositoryProperties != nil { - ha.Spec.HumioRepositoryProperties.IngestToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.HumioRepositoryProperties.IngestToken, ha.Spec.HumioRepositoryProperties.IngestTokenSource) + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.HumioRepositoryProperties.IngestToken, ha.Spec.HumioRepositoryProperties.IngestTokenSource) if err != nil { return fmt.Errorf("humioRepositoryProperties.ingestTokenSource.%v", err) } } + if ha.Spec.PagerDutyProperties != nil { + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.PagerDutyProperties.RoutingKey, ha.Spec.PagerDutyProperties.RoutingKeySource) + if err != nil { + return fmt.Errorf("pagerDutyProperties.routingKeySource.%v", err) + } + } + + if ha.Spec.VictorOpsProperties != nil { + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.VictorOpsProperties.NotifyUrl, ha.Spec.VictorOpsProperties.NotifyUrlSource) + if err != nil { + return fmt.Errorf("victorOpsProperties.notifyUrlSource.%v", err) + } + } + + if ha.Spec.WebhookProperties != nil { + apiToken, err = r.resolveField(ctx, ha.Namespace, ha.Spec.WebhookProperties.Url, ha.Spec.WebhookProperties.UrlSource) + if err != nil { + return fmt.Errorf("webhookProperties.UrlSource.%v", err) + } + + allWebhookActionHeaders := map[string]string{} + if ha.Spec.WebhookProperties.SecretHeaders != nil { + for i := range ha.Spec.WebhookProperties.SecretHeaders { + headerName := ha.Spec.WebhookProperties.SecretHeaders[i].Name + headerValueSource := ha.Spec.WebhookProperties.SecretHeaders[i].ValueFrom + allWebhookActionHeaders[headerName], err = r.resolveField(ctx, ha.Namespace, "", headerValueSource) + if err != nil { + return fmt.Errorf("webhookProperties.secretHeaders.%v", err) + } + } + + } + kubernetes.StoreFullSetOfMergedWebhookActionHeaders(ha, allWebhookActionHeaders) + } + + kubernetes.StoreSingleSecretForHa(ha, apiToken) + return nil } diff --git a/controllers/suite/resources/humioresources_controller_test.go b/controllers/suite/resources/humioresources_controller_test.go index e2e88184..c1803c31 100644 --- a/controllers/suite/resources/humioresources_controller_test.go +++ b/controllers/suite/resources/humioresources_controller_test.go @@ -19,6 +19,7 @@ package resources import ( "context" "fmt" + "github.com/humio/humio-operator/pkg/kubernetes" "net/http" "os" @@ -38,7 +39,6 @@ import ( ) var _ = Describe("Humio Resources Controllers", func() { - BeforeEach(func() { // failed test runs that don't clean up leave resources behind. humioClient.ClearHumioClientConnections() @@ -231,7 +231,6 @@ var _ = Describe("Humio Resources Controllers", func() { err := k8sClient.Get(ctx, key, fetchedIngestToken) return k8serrors.IsNotFound(err) }, testTimeout, suite.TestInterval).Should(BeTrue()) - }) It("Creating ingest token pointing to non-existent managed cluster", func() { @@ -542,7 +541,6 @@ var _ = Describe("Humio Resources Controllers", func() { suite.UsingClusterBy(clusterKey.Name, fmt.Sprintf("Waiting for repo to get deleted. Current status: %#+v", fetchedRepo.Status)) return k8serrors.IsNotFound(err) }, testTimeout, suite.TestInterval).Should(BeTrue()) - }) }) @@ -659,7 +657,6 @@ var _ = Describe("Humio Resources Controllers", func() { err := k8sClient.Get(ctx, key, fetchedParser) return k8serrors.IsNotFound(err) }, testTimeout, suite.TestInterval).Should(BeTrue()) - }) }) @@ -689,7 +686,6 @@ var _ = Describe("Humio Resources Controllers", func() { if protocol == "https" { toCreateExternalCluster.Spec.CASecretName = clusterKey.Name - } else { toCreateExternalCluster.Spec.Insecure = true } @@ -945,7 +941,7 @@ var _ = Describe("Humio Resources Controllers", func() { } key := types.NamespacedName{ - Name: "humioaction", + Name: "humioemailaction", Namespace: clusterKey.Namespace, } @@ -973,15 +969,6 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.EmailProperties.Recipients).To(Equal(toCreateAction.Spec.EmailProperties.Recipients)) - suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the action successfully") updatedAction := toCreateAction updatedAction.Spec.EmailProperties.Recipients = []string{"updated@example.com"} @@ -996,7 +983,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1005,16 +992,15 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.EmailAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.EmailAction{} + return "" } - return updatedAction.EmailAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.EmailAction)) + return updatedAction2.EmailAction.BodyTemplate + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.EmailProperties.BodyTemplate)) + Expect(updatedAction2.EmailAction.SubjectTemplate).Should(BeEquivalentTo(updatedAction.Spec.EmailProperties.SubjectTemplate)) + Expect(updatedAction2.EmailAction.Recipients).Should(BeEquivalentTo(updatedAction.Spec.EmailProperties.Recipients)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1027,17 +1013,18 @@ var _ = Describe("Humio Resources Controllers", func() { It("should handle humio repo action correctly", func() { ctx := context.Background() suite.UsingClusterBy(clusterKey.Name, "HumioAction: Should handle humio repo action correctly") + expectedSecretValue := "some-token" humioRepoActionSpec := humiov1alpha1.HumioActionSpec{ ManagedClusterName: clusterKey.Name, Name: "example-humio-repo-action", ViewName: testRepo.Spec.Name, HumioRepositoryProperties: &humiov1alpha1.HumioActionRepositoryProperties{ - IngestToken: "some-token", + IngestToken: expectedSecretValue, }, } key := types.NamespacedName{ - Name: "humioaction", + Name: "humiorepoaction", Namespace: clusterKey.Namespace, } @@ -1065,14 +1052,10 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.HumioRepositoryProperties.IngestToken).To(Equal(toCreateAction.Spec.HumioRepositoryProperties.IngestToken)) + // Should not be setting the API token in this case, but the secretMap should have the value + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the humio repo action successfully") updatedAction := toCreateAction @@ -1086,7 +1069,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the humio repo action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1094,16 +1077,13 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the humio repo action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.HumioRepoAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.HumioRepoAction{} + return "" } - return updatedAction.HumioRepoAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.HumioRepoAction)) + return updatedAction2.HumioRepoAction.IngestToken + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.HumioRepositoryProperties.IngestToken)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1116,12 +1096,13 @@ var _ = Describe("Humio Resources Controllers", func() { It("should handle ops genie action correctly", func() { ctx := context.Background() suite.UsingClusterBy(clusterKey.Name, "HumioAction: Should handle ops genie action correctly") + expectedSecretValue := "somegeniekey" opsGenieActionSpec := humiov1alpha1.HumioActionSpec{ ManagedClusterName: clusterKey.Name, Name: "example-ops-genie-action", ViewName: testRepo.Spec.Name, OpsGenieProperties: &humiov1alpha1.HumioActionOpsGenieProperties{ - GenieKey: "somegeniekey", + GenieKey: expectedSecretValue, ApiUrl: fmt.Sprintf("https://%s", testService1.Name), }, } @@ -1155,15 +1136,10 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.OpsGenieProperties.GenieKey).To(Equal(toCreateAction.Spec.OpsGenieProperties.GenieKey)) - Expect(createdAction.Spec.OpsGenieProperties.ApiUrl).To(Equal(toCreateAction.Spec.OpsGenieProperties.ApiUrl)) + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the ops genie action successfully") updatedAction := toCreateAction @@ -1178,7 +1154,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the ops genie action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1186,16 +1162,14 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the ops genie action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.OpsGenieAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.OpsGenieAction{} + return "" } - return updatedAction.OpsGenieAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.OpsGenieAction)) + return updatedAction2.OpsGenieAction.GenieKey + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.OpsGenieProperties.GenieKey)) + Expect(updatedAction2.OpsGenieAction.ApiUrl).Should(BeEquivalentTo(updatedAction.Spec.OpsGenieProperties.ApiUrl)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1208,13 +1182,14 @@ var _ = Describe("Humio Resources Controllers", func() { It("should handle pagerduty action correctly", func() { ctx := context.Background() suite.UsingClusterBy(clusterKey.Name, "HumioAction: Should handle pagerduty action correctly") + expectedSecretValue := "someroutingkey" pagerDutyActionSpec := humiov1alpha1.HumioActionSpec{ ManagedClusterName: clusterKey.Name, Name: "example-pagerduty-action", ViewName: testRepo.Spec.Name, PagerDutyProperties: &humiov1alpha1.HumioActionPagerDutyProperties{ Severity: "critical", - RoutingKey: "someroutingkey", + RoutingKey: expectedSecretValue, }, } @@ -1247,15 +1222,9 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.PagerDutyProperties.Severity).To(Equal(toCreateAction.Spec.PagerDutyProperties.Severity)) - Expect(createdAction.Spec.PagerDutyProperties.RoutingKey).To(Equal(toCreateAction.Spec.PagerDutyProperties.RoutingKey)) + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the pagerduty action successfully") updatedAction := toCreateAction @@ -1270,7 +1239,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the pagerduty action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1278,16 +1247,14 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the pagerduty action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.PagerDutyAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.PagerDutyAction{} + return "" } - return updatedAction.PagerDutyAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.PagerDutyAction)) + return updatedAction2.PagerDutyAction.RoutingKey + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.PagerDutyProperties.RoutingKey)) + Expect(updatedAction2.PagerDutyAction.Severity).Should(BeEquivalentTo(updatedAction.Spec.PagerDutyProperties.Severity)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1342,23 +1309,19 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.SlackPostMessageProperties.ApiToken).To(Equal(toCreateAction.Spec.SlackPostMessageProperties.ApiToken)) - Expect(createdAction.Spec.SlackPostMessageProperties.Channels).To(Equal(toCreateAction.Spec.SlackPostMessageProperties.Channels)) - Expect(createdAction.Spec.SlackPostMessageProperties.Fields).To(Equal(toCreateAction.Spec.SlackPostMessageProperties.Fields)) + // Check the secretMap rather than the apiToken in the ha. + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.SlackPostMessageProperties.ApiToken)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the slack post message action successfully") updatedAction := toCreateAction + updatedFieldKey := "some" + updatedFieldValue := "updatedvalue" updatedAction.Spec.SlackPostMessageProperties.ApiToken = "updated-token" updatedAction.Spec.SlackPostMessageProperties.Channels = []string{"#some-channel", "#other-channel"} updatedAction.Spec.SlackPostMessageProperties.Fields = map[string]string{ - "some": "updatedkey", + updatedFieldKey: updatedFieldValue, } suite.UsingClusterBy(clusterKey.Name, "HumioAction: Waiting for the slack post message action to be updated") @@ -1369,7 +1332,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the slack post message action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1377,16 +1340,18 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the slack post message action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.SlackPostMessageAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.SlackPostMessageAction{} + return "" } - return updatedAction.SlackPostMessageAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.SlackPostMessageAction)) + return updatedAction2.SlackPostMessageAction.ApiToken + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.SlackPostMessageProperties.ApiToken)) + Expect(updatedAction2.SlackPostMessageAction.Channels).Should(BeEquivalentTo(updatedAction.Spec.SlackPostMessageProperties.Channels)) + Expect(updatedAction2.SlackPostMessageAction.Fields).Should(BeEquivalentTo([]humioapi.SlackFieldEntryInput{{ + FieldName: updatedFieldKey, + Value: updatedFieldValue, + }})) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1440,21 +1405,18 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.SlackProperties.Url).To(Equal(toCreateAction.Spec.SlackProperties.Url)) - Expect(createdAction.Spec.SlackProperties.Fields).To(Equal(toCreateAction.Spec.SlackProperties.Fields)) + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.SlackProperties.Url)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the slack action successfully") updatedAction := toCreateAction + updatedFieldKey := "some" + updatedFieldValue := "updatedvalue" updatedAction.Spec.SlackProperties.Url = fmt.Sprintf("https://%s/services/T00000000/B00000000/YYYYYYYYYYYYYYYYYYYYYYYY", testService1.Name) updatedAction.Spec.SlackProperties.Fields = map[string]string{ - "some": "updatedkey", + updatedFieldKey: updatedFieldValue, } suite.UsingClusterBy(clusterKey.Name, "HumioAction: Waiting for the slack action to be updated") @@ -1465,7 +1427,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the slack action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1473,16 +1435,17 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the slack action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.SlackAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.SlackAction{} + return "" } - return updatedAction.SlackAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.SlackAction)) + return updatedAction2.SlackAction.Url + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.SlackProperties.Url)) + Expect(updatedAction2.SlackAction.Fields).Should(BeEquivalentTo([]humioapi.SlackFieldEntryInput{{ + FieldName: updatedFieldKey, + Value: updatedFieldValue, + }})) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1490,7 +1453,6 @@ var _ = Describe("Humio Resources Controllers", func() { err := k8sClient.Get(ctx, key, fetchedAction) return k8serrors.IsNotFound(err) }, testTimeout, suite.TestInterval).Should(BeTrue()) - }) It("should handle victor ops action correctly", func() { @@ -1535,15 +1497,10 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.VictorOpsProperties.MessageType).To(Equal(toCreateAction.Spec.VictorOpsProperties.MessageType)) - Expect(createdAction.Spec.VictorOpsProperties.NotifyUrl).To(Equal(toCreateAction.Spec.VictorOpsProperties.NotifyUrl)) + // Check the SecretMap rather than the NotifyUrl on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.VictorOpsProperties.NotifyUrl)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the victor ops action successfully") updatedAction := toCreateAction @@ -1558,7 +1515,7 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the victor ops action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction2 *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1566,16 +1523,14 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the victor ops action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.VictorOpsAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + Eventually(func() string { + updatedAction2, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) if err != nil { - return humioapi.VictorOpsAction{} + return "" } - return updatedAction.VictorOpsAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.VictorOpsAction)) + return updatedAction2.VictorOpsAction.MessageType + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedAction.Spec.VictorOpsProperties.MessageType)) + Expect(updatedAction2.VictorOpsAction.NotifyUrl).Should(BeEquivalentTo(updatedAction.Spec.VictorOpsProperties.NotifyUrl)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1587,7 +1542,7 @@ var _ = Describe("Humio Resources Controllers", func() { It("should handle web hook action correctly", func() { ctx := context.Background() - suite.UsingClusterBy(clusterKey.Name, "HumioAction: Should handle web hook action correctly") + suite.UsingClusterBy(clusterKey.Name, "HumioAction: Should handle web hook action with url directly") webHookActionSpec := humiov1alpha1.HumioActionSpec{ ManagedClusterName: clusterKey.Name, Name: "example-webhook-action", @@ -1629,34 +1584,25 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - originalAction, err := humio.ActionFromActionCR(toCreateAction) - Expect(err).To(BeNil()) - Expect(action.Name).To(Equal(originalAction.Name)) - - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.WebhookProperties.Headers).To(Equal(toCreateAction.Spec.WebhookProperties.Headers)) - Expect(createdAction.Spec.WebhookProperties.BodyTemplate).To(Equal(toCreateAction.Spec.WebhookProperties.BodyTemplate)) - Expect(createdAction.Spec.WebhookProperties.Method).To(Equal(toCreateAction.Spec.WebhookProperties.Method)) - Expect(createdAction.Spec.WebhookProperties.Url).To(Equal(toCreateAction.Spec.WebhookProperties.Url)) - suite.UsingClusterBy(clusterKey.Name, "HumioAction: Updating the web hook action successfully") - updatedAction := toCreateAction - updatedAction.Spec.WebhookProperties.Headers = map[string]string{"updated": "header"} - updatedAction.Spec.WebhookProperties.BodyTemplate = "updated template" - updatedAction.Spec.WebhookProperties.Method = http.MethodPut - updatedAction.Spec.WebhookProperties.Url = fmt.Sprintf("https://%s/some/updated/api", testService1.Name) + updatedHeaderKey := "updatedKey" + updatedHeaderValue := "updatedValue" + updatedWebhookActionProperties := &humiov1alpha1.HumioActionWebhookProperties{ + Headers: map[string]string{updatedHeaderKey: updatedHeaderValue}, + BodyTemplate: "updated template", + Method: http.MethodPut, + Url: fmt.Sprintf("https://%s/some/updated/api", testService1.Name), + } suite.UsingClusterBy(clusterKey.Name, "HumioAction: Waiting for the web hook action to be updated") Eventually(func() error { k8sClient.Get(ctx, key, fetchedAction) - fetchedAction.Spec.WebhookProperties = updatedAction.Spec.WebhookProperties + fetchedAction.Spec.WebhookProperties = updatedWebhookActionProperties return k8sClient.Update(ctx, fetchedAction) }, testTimeout, suite.TestInterval).Should(Succeed()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the web hook action update succeeded") - var expectedUpdatedAction *humioapi.Action + var expectedUpdatedAction, updatedAction *humioapi.Action Eventually(func() error { expectedUpdatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) return err @@ -1664,16 +1610,19 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(expectedUpdatedAction).ToNot(BeNil()) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Verifying the web hook action matches the expected") - verifiedAction, err := humio.ActionFromActionCR(updatedAction) - Expect(err).To(BeNil()) - Expect(verifiedAction).ToNot(BeNil()) - Eventually(func() humioapi.WebhookAction { - updatedAction, err := humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) - if err != nil { - return humioapi.WebhookAction{} + Eventually(func() string { + updatedAction, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, fetchedAction) + if err != nil || updatedAction == nil { + return "" } - return updatedAction.WebhookAction - }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(verifiedAction.WebhookAction)) + return updatedAction.WebhookAction.Url + }, testTimeout, suite.TestInterval).Should(BeEquivalentTo(updatedWebhookActionProperties.Url)) + Expect(updatedAction.WebhookAction.Headers).Should(BeEquivalentTo([]humioapi.HttpHeaderEntryInput{{ + Header: updatedHeaderKey, + Value: updatedHeaderValue, + }})) + Expect(updatedAction.WebhookAction.BodyTemplate).To(BeEquivalentTo(updatedWebhookActionProperties.BodyTemplate)) + Expect(updatedAction.WebhookAction.Method).To(BeEquivalentTo(updatedWebhookActionProperties.Method)) suite.UsingClusterBy(clusterKey.Name, "HumioAction: Successfully deleting it") Expect(k8sClient.Delete(ctx, fetchedAction)).To(Succeed()) @@ -1686,7 +1635,7 @@ var _ = Describe("Humio Resources Controllers", func() { It("HumioAction: Should deny improperly configured action with missing properties", func() { ctx := context.Background() key := types.NamespacedName{ - Name: "humio-webhook-action", + Name: "humio-webhook-action-missing", Namespace: clusterKey.Namespace, } @@ -1729,7 +1678,7 @@ var _ = Describe("Humio Resources Controllers", func() { It("HumioAction: Should deny improperly configured action with extra properties", func() { ctx := context.Background() key := types.NamespacedName{ - Name: "humio-webhook-action", + Name: "humio-webhook-action-extra", Namespace: clusterKey.Namespace, } toCreateInvalidAction := &humiov1alpha1.HumioAction{ @@ -1799,13 +1748,14 @@ var _ = Describe("Humio Resources Controllers", func() { }, } + expectedSecretValue := "secret-token" secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "action-humio-repository-secret", Namespace: clusterKey.Namespace, }, Data: map[string][]byte{ - "key": []byte("secret-token"), + "key": []byte(expectedSecretValue), }, } @@ -1825,10 +1775,10 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.HumioRepositoryProperties.IngestToken).To(Equal("secret-token")) + // Should not be setting the API token in this case, but the secretMap should have the value + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) }) It("HumioAction: OpsGenieProperties: Should support referencing secrets", func() { @@ -1861,13 +1811,14 @@ var _ = Describe("Humio Resources Controllers", func() { }, } + expectedSecretValue := "secret-token" secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "action-genie-secret", Namespace: clusterKey.Namespace, }, Data: map[string][]byte{ - "key": []byte("secret-token"), + "key": []byte(expectedSecretValue), }, } @@ -1887,11 +1838,10 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.OpsGenieProperties.GenieKey).To(Equal("secret-token")) - Expect(createdAction.Spec.OpsGenieProperties.ApiUrl).To(Equal(fmt.Sprintf("https://%s", testService1.Name))) + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) }) It("HumioAction: OpsGenieProperties: Should support direct genie key", func() { @@ -1901,6 +1851,7 @@ var _ = Describe("Humio Resources Controllers", func() { Namespace: clusterKey.Namespace, } + expectedSecretValue := "direct-token" toCreateAction := &humiov1alpha1.HumioAction{ ObjectMeta: metav1.ObjectMeta{ Name: key.Name, @@ -1911,7 +1862,7 @@ var _ = Describe("Humio Resources Controllers", func() { Name: key.Name, ViewName: testRepo.Spec.Name, OpsGenieProperties: &humiov1alpha1.HumioActionOpsGenieProperties{ - GenieKey: "direct-token", + GenieKey: expectedSecretValue, ApiUrl: fmt.Sprintf("https://%s", testService1.Name), }, }, @@ -1932,11 +1883,118 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.OpsGenieProperties.GenieKey).To(Equal("direct-token")) - Expect(createdAction.Spec.OpsGenieProperties.ApiUrl).To(Equal(fmt.Sprintf("https://%s", testService1.Name))) + // Should not be setting the API token in this case, but the secretMap should have the value + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) + }) + + It("HumioAction: VictorOpsProperties: Should support referencing secrets", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "victorops-action-secret", + Namespace: clusterKey.Namespace, + } + + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + VictorOpsProperties: &humiov1alpha1.HumioActionVictorOpsProperties{ + MessageType: "critical", + NotifyUrlSource: humiov1alpha1.VarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "action-victorops-secret", + }, + Key: "key", + }, + }, + }, + }, + } + + expectedSecretValue := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "action-victorops-secret", + Namespace: clusterKey.Namespace, + }, + Data: map[string][]byte{ + "key": []byte(expectedSecretValue), + }, + } + + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) + }) + + It("HumioAction: VictorOpsProperties: Should support direct notify url", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "victorops-action-direct", + Namespace: clusterKey.Namespace, + } + + expectedSecretValue := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + VictorOpsProperties: &humiov1alpha1.HumioActionVictorOpsProperties{ + MessageType: "critical", + NotifyUrl: expectedSecretValue, + }, + }, + } + + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Check the SecretMap rather than the NotifyUrl on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) }) It("HumioAction: SlackPostMessageProperties: Should support referencing secrets", func() { @@ -1972,13 +2030,14 @@ var _ = Describe("Humio Resources Controllers", func() { }, } + expectedSecretValue := "secret-token" secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "action-slack-post-secret", Namespace: clusterKey.Namespace, }, Data: map[string][]byte{ - "key": []byte("secret-token"), + "key": []byte(expectedSecretValue), }, } @@ -1998,10 +2057,10 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.SlackPostMessageProperties.ApiToken).To(Equal("secret-token")) + // Should not be setting the API token in this case, but the secretMap should have the value + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) }) It("HumioAction: SlackPostMessageProperties: Should support direct api token", func() { @@ -2045,103 +2104,669 @@ var _ = Describe("Humio Resources Controllers", func() { }, testTimeout, suite.TestInterval).Should(Succeed()) Expect(action).ToNot(BeNil()) - createdAction, err := humio.CRActionFromAPIAction(action) - Expect(err).To(BeNil()) - Expect(createdAction.Spec.Name).To(Equal(toCreateAction.Spec.Name)) - Expect(createdAction.Spec.SlackPostMessageProperties.ApiToken).To(Equal("direct-token")) + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.SlackPostMessageProperties.ApiToken)) }) - }) - Context("Humio Alert", func() { - It("should handle alert action correctly", func() { + It("HumioAction: SlackProperties: Should support referencing secrets", func() { ctx := context.Background() - suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Should handle alert correctly") - dependentEmailActionSpec := humiov1alpha1.HumioActionSpec{ - ManagedClusterName: clusterKey.Name, - Name: "example-email-action", - ViewName: testRepo.Spec.Name, - EmailProperties: &humiov1alpha1.HumioActionEmailProperties{ - Recipients: []string{"example@example.com"}, - }, + key := types.NamespacedName{ + Name: "humio-slack-action-secret", + Namespace: clusterKey.Namespace, } - actionKey := types.NamespacedName{ - Name: "humioaction", - Namespace: clusterKey.Namespace, + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + SlackProperties: &humiov1alpha1.HumioActionSlackProperties{ + UrlSource: humiov1alpha1.VarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "action-slack-secret-from-secret", + }, + Key: "key", + }, + }, + Fields: map[string]string{ + "some": "key", + }, + }, + }, } - toCreateDependentAction := &humiov1alpha1.HumioAction{ + expectedSecretValue := "https://hooks.slack.com/services/T00000000/B00000000/YYYYYYYYYYYYYYYYYYYYYYYY" + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: actionKey.Name, - Namespace: actionKey.Namespace, + Name: toCreateAction.Spec.SlackProperties.UrlSource.SecretKeyRef.LocalObjectReference.Name, + Namespace: clusterKey.Namespace, + }, + Data: map[string][]byte{ + toCreateAction.Spec.SlackProperties.UrlSource.SecretKeyRef.Key: []byte(expectedSecretValue), }, - Spec: dependentEmailActionSpec, } - suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Creating the action required by the alert successfully") - Expect(k8sClient.Create(ctx, toCreateDependentAction)).Should(Succeed()) + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) fetchedAction := &humiov1alpha1.HumioAction{} Eventually(func() string { - k8sClient.Get(ctx, actionKey, fetchedAction) + k8sClient.Get(ctx, key, fetchedAction) return fetchedAction.Status.State }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) - alertSpec := humiov1alpha1.HumioAlertSpec{ - ManagedClusterName: clusterKey.Name, - Name: "example-alert", - ViewName: testRepo.Spec.Name, - Query: humiov1alpha1.HumioQuery{ - QueryString: "#repo = test | count()", - Start: "1d", - }, - ThrottleTimeMillis: 60000, - ThrottleField: "some field", - Silenced: false, - Description: "humio alert", - Actions: []string{toCreateDependentAction.Spec.Name}, - Labels: []string{"some-label"}, - } + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Should not be setting the API token in this case, but the secretMap should have the value + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) + }) + It("HumioAction: SlackProperties: Should support direct url", func() { + ctx := context.Background() key := types.NamespacedName{ - Name: "humio-alert", + Name: "humio-slack-action-direct", Namespace: clusterKey.Namespace, } - toCreateAlert := &humiov1alpha1.HumioAlert{ + expectedSecretValue := "https://hooks.slack.com/services/T00000000/B00000000/YYYYYYYYYYYYYYYYYYYYYYYY" + toCreateAction := &humiov1alpha1.HumioAction{ ObjectMeta: metav1.ObjectMeta{ Name: key.Name, Namespace: key.Namespace, }, - Spec: alertSpec, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + SlackProperties: &humiov1alpha1.HumioActionSlackProperties{ + Url: expectedSecretValue, + Fields: map[string]string{ + "some": "key", + }, + }, + }, } - suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Creating the alert successfully") - Expect(k8sClient.Create(ctx, toCreateAlert)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) - fetchedAlert := &humiov1alpha1.HumioAlert{} + fetchedAction := &humiov1alpha1.HumioAction{} Eventually(func() string { - k8sClient.Get(ctx, key, fetchedAlert) - return fetchedAlert.Status.State - }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioAlertStateExists)) + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) - var alert *humioapi.Alert + var action *humioapi.Action Eventually(func() error { - alert, err = humioClient.GetAlert(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAlert) + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) return err }, testTimeout, suite.TestInterval).Should(Succeed()) - Expect(alert).ToNot(BeNil()) + Expect(action).ToNot(BeNil()) - var actionIdMap map[string]string - Eventually(func() error { - actionIdMap, err = humioClient.GetActionIDsMapForAlerts(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAlert) - return err - }, testTimeout, suite.TestInterval).Should(Succeed()) + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.SlackProperties.Url)) + }) - originalAlert, err := humio.AlertTransform(toCreateAlert, actionIdMap) - Expect(err).To(BeNil()) - Expect(alert.Name).To(Equal(originalAlert.Name)) - Expect(alert.Description).To(Equal(originalAlert.Description)) + It("HumioAction: PagerDutyProperties: Should support referencing secrets", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-pagerduty-action-secret", + Namespace: clusterKey.Namespace, + } + + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + PagerDutyProperties: &humiov1alpha1.HumioActionPagerDutyProperties{ + RoutingKeySource: humiov1alpha1.VarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "action-pagerduty-secret", + }, + Key: "key", + }, + }, + Severity: "critical", + }, + }, + } + + expectedSecretValue := "secret-key" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "action-pagerduty-secret", + Namespace: clusterKey.Namespace, + }, + Data: map[string][]byte{ + "key": []byte(expectedSecretValue), + }, + } + + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) + }) + + It("HumioAction: PagerDutyProperties: Should support direct api token", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-pagerduty-action-direct", + Namespace: clusterKey.Namespace, + } + + expectedSecretValue := "direct-routing-key" + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + PagerDutyProperties: &humiov1alpha1.HumioActionPagerDutyProperties{ + RoutingKey: expectedSecretValue, + Severity: "critical", + }, + }, + } + + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Check the secretMap rather than the apiToken in the ha. + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.PagerDutyProperties.RoutingKey)) + }) + + It("HumioAction: WebhookProperties: Should support direct url", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-webhook-action-direct", + Namespace: clusterKey.Namespace, + } + + expectedSecretValue := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + WebhookProperties: &humiov1alpha1.HumioActionWebhookProperties{ + BodyTemplate: "body template", + Method: http.MethodPost, + Url: expectedSecretValue, + }, + }, + } + + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.WebhookProperties.Url)) + }) + + It("HumioAction: WebhookProperties: Should support referencing secret url", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-webhook-action-secret", + Namespace: clusterKey.Namespace, + } + + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + WebhookProperties: &humiov1alpha1.HumioActionWebhookProperties{ + BodyTemplate: "body template", + Method: http.MethodPost, + UrlSource: humiov1alpha1.VarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "action-webhook-url-secret", + }, + Key: "key", + }, + }, + }, + }, + } + + expectedSecretValue := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "action-webhook-url-secret", + Namespace: clusterKey.Namespace, + }, + Data: map[string][]byte{ + "key": []byte(expectedSecretValue), + }, + } + + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(expectedSecretValue)) + }) + + It("HumioAction: WebhookProperties: Should support direct url and headers", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-webhook-action-with-headers", + Namespace: clusterKey.Namespace, + } + + expectedUrl := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + nonsensitiveHeaderKey := "foo" + nonsensitiveHeaderValue := "bar" + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + WebhookProperties: &humiov1alpha1.HumioActionWebhookProperties{ + BodyTemplate: "body template", + Method: http.MethodPost, + Url: expectedUrl, + Headers: map[string]string{ + nonsensitiveHeaderKey: nonsensitiveHeaderValue, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + Expect(action.WebhookAction.Url).To(Equal(expectedUrl)) + Expect(action.WebhookAction.Headers).Should(ContainElements([]humioapi.HttpHeaderEntryInput{ + { + Header: nonsensitiveHeaderKey, + Value: nonsensitiveHeaderValue, + }, + })) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.WebhookProperties.Url)) + + allHeaders, found := kubernetes.GetFullSetOfMergedWebhookheaders(toCreateAction) + Expect(found).To(BeTrue()) + Expect(allHeaders).To(HaveKeyWithValue(nonsensitiveHeaderKey, nonsensitiveHeaderValue)) + }) + It("HumioAction: WebhookProperties: Should support direct url and mixed headers", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-webhook-action-with-mixed-headers", + Namespace: clusterKey.Namespace, + } + + expectedUrl := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + headerKey1 := "foo1" + sensitiveHeaderValue1 := "bar1" + headerKey2 := "foo2" + nonsensitiveHeaderValue2 := "bar2" + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + WebhookProperties: &humiov1alpha1.HumioActionWebhookProperties{ + BodyTemplate: "body template", + Method: http.MethodPost, + Url: expectedUrl, + Headers: map[string]string{ + headerKey2: nonsensitiveHeaderValue2, + }, + SecretHeaders: []humiov1alpha1.HeadersSource{ + { + Name: headerKey1, + ValueFrom: humiov1alpha1.VarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "action-webhook-header-secret-mixed", + }, + Key: "key", + }, + }, + }, + }, + }, + }, + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "action-webhook-header-secret-mixed", + Namespace: clusterKey.Namespace, + }, + Data: map[string][]byte{ + "key": []byte(sensitiveHeaderValue1), + }, + } + + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + Expect(action.WebhookAction.Url).To(Equal(expectedUrl)) + Expect(action.WebhookAction.Headers).Should(ContainElements([]humioapi.HttpHeaderEntryInput{ + { + Header: headerKey1, + Value: sensitiveHeaderValue1, + }, + { + Header: headerKey2, + Value: nonsensitiveHeaderValue2, + }, + })) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.WebhookProperties.Url)) + + allHeaders, found := kubernetes.GetFullSetOfMergedWebhookheaders(toCreateAction) + Expect(found).To(BeTrue()) + Expect(allHeaders).To(HaveKeyWithValue(headerKey1, sensitiveHeaderValue1)) + Expect(allHeaders).To(HaveKeyWithValue(headerKey2, nonsensitiveHeaderValue2)) + }) + It("HumioAction: WebhookProperties: Should support direct url and secret headers", func() { + ctx := context.Background() + key := types.NamespacedName{ + Name: "humio-webhook-action-with-secret-headers", + Namespace: clusterKey.Namespace, + } + + expectedUrl := fmt.Sprintf("https://%s/integrations/0000/alert/0000/routing_key", testService1.Name) + headerKey := "foo" + sensitiveHeaderValue := "bar" + toCreateAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: key.Name, + ViewName: testRepo.Spec.Name, + WebhookProperties: &humiov1alpha1.HumioActionWebhookProperties{ + BodyTemplate: "body template", + Method: http.MethodPost, + Url: expectedUrl, + SecretHeaders: []humiov1alpha1.HeadersSource{ + { + Name: headerKey, + ValueFrom: humiov1alpha1.VarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "action-webhook-header-secret", + }, + Key: "key", + }, + }, + }, + }, + }, + }, + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "action-webhook-header-secret", + Namespace: clusterKey.Namespace, + }, + Data: map[string][]byte{ + "key": []byte(sensitiveHeaderValue), + }, + } + + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + Expect(k8sClient.Create(ctx, toCreateAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + var action *humioapi.Action + Eventually(func() error { + action, err = humioClient.GetAction(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAction) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(action).ToNot(BeNil()) + Expect(action.WebhookAction.Url).To(Equal(expectedUrl)) + Expect(action.WebhookAction.Headers).Should(ContainElements([]humioapi.HttpHeaderEntryInput{ + { + Header: headerKey, + Value: sensitiveHeaderValue, + }, + })) + + // Check the SecretMap rather than the ApiToken on the action + apiToken, found := kubernetes.GetSecretForHa(toCreateAction) + Expect(found).To(BeTrue()) + Expect(apiToken).To(Equal(toCreateAction.Spec.WebhookProperties.Url)) + + allHeaders, found := kubernetes.GetFullSetOfMergedWebhookheaders(toCreateAction) + Expect(found).To(BeTrue()) + Expect(allHeaders).To(HaveKeyWithValue(headerKey, sensitiveHeaderValue)) + }) + }) + + Context("Humio Alert", func() { + It("should handle alert action correctly", func() { + ctx := context.Background() + suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Should handle alert correctly") + dependentEmailActionSpec := humiov1alpha1.HumioActionSpec{ + ManagedClusterName: clusterKey.Name, + Name: "example-email-action", + ViewName: testRepo.Spec.Name, + EmailProperties: &humiov1alpha1.HumioActionEmailProperties{ + Recipients: []string{"example@example.com"}, + }, + } + + actionKey := types.NamespacedName{ + Name: "humiorepoactionforalert", + Namespace: clusterKey.Namespace, + } + + toCreateDependentAction := &humiov1alpha1.HumioAction{ + ObjectMeta: metav1.ObjectMeta{ + Name: actionKey.Name, + Namespace: actionKey.Namespace, + }, + Spec: dependentEmailActionSpec, + } + + suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Creating the action required by the alert successfully") + Expect(k8sClient.Create(ctx, toCreateDependentAction)).Should(Succeed()) + + fetchedAction := &humiov1alpha1.HumioAction{} + Eventually(func() string { + k8sClient.Get(ctx, actionKey, fetchedAction) + return fetchedAction.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioActionStateExists)) + + alertSpec := humiov1alpha1.HumioAlertSpec{ + ManagedClusterName: clusterKey.Name, + Name: "example-alert", + ViewName: testRepo.Spec.Name, + Query: humiov1alpha1.HumioQuery{ + QueryString: "#repo = test | count()", + Start: "1d", + }, + ThrottleTimeMillis: 60000, + ThrottleField: "some field", + Silenced: false, + Description: "humio alert", + Actions: []string{toCreateDependentAction.Spec.Name}, + Labels: []string{"some-label"}, + } + + key := types.NamespacedName{ + Name: "humio-alert", + Namespace: clusterKey.Namespace, + } + + toCreateAlert := &humiov1alpha1.HumioAlert{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: alertSpec, + } + + suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Creating the alert successfully") + Expect(k8sClient.Create(ctx, toCreateAlert)).Should(Succeed()) + + fetchedAlert := &humiov1alpha1.HumioAlert{} + Eventually(func() string { + k8sClient.Get(ctx, key, fetchedAlert) + return fetchedAlert.Status.State + }, testTimeout, suite.TestInterval).Should(Equal(humiov1alpha1.HumioAlertStateExists)) + + var alert *humioapi.Alert + Eventually(func() error { + alert, err = humioClient.GetAlert(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAlert) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + Expect(alert).ToNot(BeNil()) + + var actionIdMap map[string]string + Eventually(func() error { + actionIdMap, err = humioClient.GetActionIDsMapForAlerts(sharedCluster.Config(), reconcile.Request{NamespacedName: clusterKey}, toCreateAlert) + return err + }, testTimeout, suite.TestInterval).Should(Succeed()) + + originalAlert, err := humio.AlertTransform(toCreateAlert, actionIdMap) + Expect(err).To(BeNil()) + Expect(alert.Name).To(Equal(originalAlert.Name)) + Expect(alert.Description).To(Equal(originalAlert.Description)) Expect(alert.Actions).To(Equal(originalAlert.Actions)) Expect(alert.Labels).To(Equal(originalAlert.Labels)) Expect(alert.ThrottleTimeMillis).To(Equal(originalAlert.ThrottleTimeMillis)) @@ -2150,11 +2775,6 @@ var _ = Describe("Humio Resources Controllers", func() { Expect(alert.QueryString).To(Equal(originalAlert.QueryString)) Expect(alert.QueryStart).To(Equal(originalAlert.QueryStart)) - createdAlert := toCreateAlert - err = humio.AlertHydrate(createdAlert, alert, actionIdMap) - Expect(err).To(BeNil()) - Expect(createdAlert.Spec).To(Equal(toCreateAlert.Spec)) - suite.UsingClusterBy(clusterKey.Name, "HumioAlert: Updating the alert successfully") updatedAlert := toCreateAlert updatedAlert.Spec.Query.QueryString = "#repo = test | updated=true | count()" diff --git a/examples/humioaction-webhook.yaml b/examples/humioaction-webhook.yaml index de08a12b..c85db2cb 100644 --- a/examples/humioaction-webhook.yaml +++ b/examples/humioaction-webhook.yaml @@ -33,3 +33,54 @@ spec: bodyTemplate: |- {alert_name} has alerted click {url} to see the alert +--- +apiVersion: core.humio.com/v1alpha1 +kind: HumioAction +metadata: + name: humio-web-hook-action-mixed-headers-external +spec: + externalClusterName: example-humioexternalcluster + name: example-web-hook-action-using-secrets + viewName: humio + webhookProperties: + urlSource: + secretKeyRef: + name: example-humiocluster-webhook-action-url-secret + key: data + headers: + some: header + some-other: header + secretHeaders: + - name: this + valueFrom: + secretKeyRef: + name: example-humiocluster-webhook-action-headers-secret + key: somesecretheader + method: POST + bodyTemplate: |- + {alert_name} has alerted + click {url} to see the alert +--- +apiVersion: core.humio.com/v1alpha1 +kind: HumioAction +metadata: + name: humio-web-hook-action-all-secret-external +spec: + externalClusterName: example-humioexternalcluster + name: example-web-hook-action-using-secret-url-and-headers + viewName: humio + webhookProperties: + urlSource: + secretKeyRef: + name: example-humiocluster-webhook-action-url-secret + key: data + secretHeaders: + - name: this + valueFrom: + secretKeyRef: + name: example-humiocluster-webhook-action-headers-secret + key: somesecretheader + method: POST + bodyTemplate: |- + {alert_name} has alerted + click {url} to see the alert diff --git a/pkg/humio/action_transform.go b/pkg/humio/action_transform.go index d21041fa..f15fa2b0 100644 --- a/pkg/humio/action_transform.go +++ b/pkg/humio/action_transform.go @@ -18,13 +18,11 @@ package humio import ( "fmt" + "github.com/humio/humio-operator/pkg/kubernetes" "net/http" "net/url" - "reflect" "strings" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - humiov1alpha1 "github.com/humio/humio-operator/api/v1alpha1" humioapi "github.com/humio/cli/api" @@ -43,113 +41,8 @@ const ( ActionTypeOpsGenie = "OpsGenie" ) -func CRActionFromAPIAction(action *humioapi.Action) (*humiov1alpha1.HumioAction, error) { - ha := &humiov1alpha1.HumioAction{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - ActionIdentifierAnnotation: action.ID, - }, - }, - Spec: humiov1alpha1.HumioActionSpec{ - Name: action.Name, - }, - } - - if !reflect.ValueOf(action.EmailAction).IsZero() { - ha.Spec.EmailProperties = &humiov1alpha1.HumioActionEmailProperties{ - Recipients: action.EmailAction.Recipients, - } - if action.EmailAction.BodyTemplate != "" { - ha.Spec.EmailProperties.BodyTemplate = action.EmailAction.BodyTemplate - } - if action.EmailAction.SubjectTemplate != "" { - ha.Spec.EmailProperties.SubjectTemplate = action.EmailAction.SubjectTemplate - } - } - - if !reflect.ValueOf(action.HumioRepoAction).IsZero() { - ha.Spec.HumioRepositoryProperties = &humiov1alpha1.HumioActionRepositoryProperties{ - IngestToken: action.HumioRepoAction.IngestToken, - } - } - - if !reflect.ValueOf(action.OpsGenieAction).IsZero() { - ha.Spec.OpsGenieProperties = &humiov1alpha1.HumioActionOpsGenieProperties{ - ApiUrl: action.OpsGenieAction.ApiUrl, - GenieKey: action.OpsGenieAction.GenieKey, - UseProxy: action.OpsGenieAction.UseProxy, - } - } - - if !reflect.ValueOf(action.PagerDutyAction).IsZero() { - ha.Spec.PagerDutyProperties = &humiov1alpha1.HumioActionPagerDutyProperties{ - RoutingKey: action.PagerDutyAction.RoutingKey, - Severity: action.PagerDutyAction.Severity, - UseProxy: action.PagerDutyAction.UseProxy, - } - } - - if !reflect.ValueOf(action.SlackAction).IsZero() { - fields := make(map[string]string) - for _, field := range action.SlackAction.Fields { - fields[field.FieldName] = field.Value - } - ha.Spec.SlackProperties = &humiov1alpha1.HumioActionSlackProperties{ - Fields: fields, - Url: action.SlackAction.Url, - UseProxy: action.SlackAction.UseProxy, - } - } - - if !reflect.ValueOf(action.SlackPostMessageAction).IsZero() { - fields := make(map[string]string) - for _, field := range action.SlackPostMessageAction.Fields { - fields[field.FieldName] = field.Value - } - ha.Spec.SlackPostMessageProperties = &humiov1alpha1.HumioActionSlackPostMessageProperties{ - ApiToken: action.SlackPostMessageAction.ApiToken, - Channels: action.SlackPostMessageAction.Channels, - Fields: fields, - UseProxy: action.SlackPostMessageAction.UseProxy, - } - } - - if !reflect.ValueOf(action.VictorOpsAction).IsZero() { - ha.Spec.VictorOpsProperties = &humiov1alpha1.HumioActionVictorOpsProperties{ - MessageType: action.VictorOpsAction.MessageType, - NotifyUrl: action.VictorOpsAction.NotifyUrl, - UseProxy: action.VictorOpsAction.UseProxy, - } - } - - if !reflect.ValueOf(action.WebhookAction).IsZero() { - headers := make(map[string]string) - for _, field := range action.WebhookAction.Headers { - headers[field.Header] = field.Value - } - ha.Spec.WebhookProperties = &humiov1alpha1.HumioActionWebhookProperties{ - BodyTemplate: action.WebhookAction.BodyTemplate, - Headers: headers, - Method: action.WebhookAction.Method, - Url: action.WebhookAction.Url, - IgnoreSSL: action.WebhookAction.IgnoreSSL, - UseProxy: action.WebhookAction.UseProxy, - } - } - if reflect.ValueOf(action.EmailAction).IsZero() && - reflect.ValueOf(action.HumioRepoAction).IsZero() && - reflect.ValueOf(action.OpsGenieAction).IsZero() && - reflect.ValueOf(action.PagerDutyAction).IsZero() && - reflect.ValueOf(action.SlackAction).IsZero() && - reflect.ValueOf(action.SlackPostMessageAction).IsZero() && - reflect.ValueOf(action.VictorOpsAction).IsZero() && - reflect.ValueOf(action.WebhookAction).IsZero() { - return nil, fmt.Errorf("no action configuration specified") - } - - return ha, nil -} - +// ActionFromActionCR converts a HumioAction Kubernetes custom resource to an Action that is valid for the LogScale API. +// It assumes any referenced secret values have been resolved by method resolveSecrets on HumioActionReconciler. func ActionFromActionCR(ha *humiov1alpha1.HumioAction) (*humioapi.Action, error) { at, err := actionType(ha) if err != nil { @@ -206,14 +99,21 @@ func humioRepoAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { return action, err } - if hn.Spec.HumioRepositoryProperties.IngestToken == "" { + apiToken, found := kubernetes.GetSecretForHa(hn) + + if hn.Spec.HumioRepositoryProperties.IngestToken == "" && !found { errorList = append(errorList, "property humioRepositoryProperties.ingestToken is required") } if len(errorList) > 0 { return ifErrors(action, ActionTypeHumioRepo, errorList) } + if hn.Spec.HumioRepositoryProperties.IngestToken != "" { + action.HumioRepoAction.IngestToken = hn.Spec.HumioRepositoryProperties.IngestToken + } else { + action.HumioRepoAction.IngestToken = apiToken + } + action.Type = humioapi.ActionTypeHumioRepo - action.HumioRepoAction.IngestToken = hn.Spec.HumioRepositoryProperties.IngestToken return action, nil } @@ -225,7 +125,9 @@ func opsGenieAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { return action, err } - if hn.Spec.OpsGenieProperties.GenieKey == "" { + apiToken, found := kubernetes.GetSecretForHa(hn) + + if hn.Spec.OpsGenieProperties.GenieKey == "" && !found { errorList = append(errorList, "property opsGenieProperties.genieKey is required") } if hn.Spec.OpsGenieProperties.ApiUrl == "" { @@ -234,8 +136,13 @@ func opsGenieAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { if len(errorList) > 0 { return ifErrors(action, ActionTypeOpsGenie, errorList) } + if hn.Spec.OpsGenieProperties.GenieKey != "" { + action.OpsGenieAction.GenieKey = hn.Spec.OpsGenieProperties.GenieKey + } else { + action.OpsGenieAction.GenieKey = apiToken + } + action.Type = humioapi.ActionTypeOpsGenie - action.OpsGenieAction.GenieKey = hn.Spec.OpsGenieProperties.GenieKey action.OpsGenieAction.ApiUrl = hn.Spec.OpsGenieProperties.ApiUrl action.OpsGenieAction.UseProxy = hn.Spec.OpsGenieProperties.UseProxy @@ -249,8 +156,10 @@ func pagerDutyAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { return action, err } + apiToken, found := kubernetes.GetSecretForHa(hn) + var severity string - if hn.Spec.PagerDutyProperties.RoutingKey == "" { + if hn.Spec.PagerDutyProperties.RoutingKey == "" && !found { errorList = append(errorList, "property pagerDutyProperties.routingKey is required") } if hn.Spec.PagerDutyProperties.Severity == "" { @@ -267,8 +176,13 @@ func pagerDutyAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { if len(errorList) > 0 { return ifErrors(action, ActionTypePagerDuty, errorList) } + if hn.Spec.PagerDutyProperties.RoutingKey != "" { + action.PagerDutyAction.RoutingKey = hn.Spec.PagerDutyProperties.RoutingKey + } else { + action.PagerDutyAction.RoutingKey = apiToken + } + action.Type = humioapi.ActionTypePagerDuty - action.PagerDutyAction.RoutingKey = hn.Spec.PagerDutyProperties.RoutingKey action.PagerDutyAction.Severity = severity action.PagerDutyAction.UseProxy = hn.Spec.PagerDutyProperties.UseProxy @@ -282,17 +196,26 @@ func slackAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { return action, err } + slackUrl, found := kubernetes.GetSecretForHa(hn) + if hn.Spec.SlackProperties.Url == "" && !found { + errorList = append(errorList, "property slackProperties.url is required") + } if hn.Spec.SlackProperties.Fields == nil { errorList = append(errorList, "property slackProperties.fields is required") } - if _, err := url.ParseRequestURI(hn.Spec.SlackProperties.Url); err != nil { + if hn.Spec.SlackProperties.Url != "" { + action.SlackAction.Url = hn.Spec.SlackProperties.Url + } else { + action.SlackAction.Url = slackUrl + } + if _, err := url.ParseRequestURI(action.SlackAction.Url); err != nil { errorList = append(errorList, fmt.Sprintf("invalid url for slackProperties.url: %s", err.Error())) } if len(errorList) > 0 { return ifErrors(action, ActionTypeSlack, errorList) } + action.Type = humioapi.ActionTypeSlack - action.SlackAction.Url = hn.Spec.SlackProperties.Url action.SlackAction.UseProxy = hn.Spec.SlackProperties.UseProxy action.SlackAction.Fields = []humioapi.SlackFieldEntryInput{} for k, v := range hn.Spec.SlackProperties.Fields { @@ -314,7 +237,8 @@ func slackPostMessageAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, er return action, err } - if hn.Spec.SlackPostMessageProperties.ApiToken == "" { + apiToken, found := kubernetes.GetSecretForHa(hn) + if hn.Spec.SlackPostMessageProperties.ApiToken == "" && !found { errorList = append(errorList, "property slackPostMessageProperties.apiToken is required") } if len(hn.Spec.SlackPostMessageProperties.Channels) == 0 { @@ -326,8 +250,13 @@ func slackPostMessageAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, er if len(errorList) > 0 { return ifErrors(action, ActionTypeSlackPostMessage, errorList) } + if hn.Spec.SlackPostMessageProperties.ApiToken != "" { + action.SlackPostMessageAction.ApiToken = hn.Spec.SlackPostMessageProperties.ApiToken + } else { + action.SlackPostMessageAction.ApiToken = apiToken + } + action.Type = humioapi.ActionTypeSlackPostMessage - action.SlackPostMessageAction.ApiToken = hn.Spec.SlackPostMessageProperties.ApiToken action.SlackPostMessageAction.Channels = hn.Spec.SlackPostMessageProperties.Channels action.SlackPostMessageAction.UseProxy = hn.Spec.SlackPostMessageProperties.UseProxy action.SlackPostMessageAction.Fields = []humioapi.SlackFieldEntryInput{} @@ -350,7 +279,12 @@ func victorOpsAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { return action, err } + apiToken, found := kubernetes.GetSecretForHa(hn) + var messageType string + if hn.Spec.VictorOpsProperties.NotifyUrl == "" && !found { + errorList = append(errorList, "property victorOpsProperties.notifyUrl is required") + } if hn.Spec.VictorOpsProperties.MessageType == "" { errorList = append(errorList, "property victorOpsProperties.messageType is required") } @@ -362,15 +296,20 @@ func victorOpsAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { hn.Spec.VictorOpsProperties.MessageType, strings.Join(acceptedMessageTypes, ", "))) } } - if _, err := url.ParseRequestURI(hn.Spec.VictorOpsProperties.NotifyUrl); err != nil { + if hn.Spec.VictorOpsProperties.NotifyUrl != "" { + action.VictorOpsAction.NotifyUrl = hn.Spec.VictorOpsProperties.NotifyUrl + } else { + action.VictorOpsAction.NotifyUrl = apiToken + } + if _, err := url.ParseRequestURI(action.VictorOpsAction.NotifyUrl); err != nil { errorList = append(errorList, fmt.Sprintf("invalid url for victorOpsProperties.notifyUrl: %s", err.Error())) } if len(errorList) > 0 { return ifErrors(action, ActionTypeVictorOps, errorList) } + action.Type = humioapi.ActionTypeVictorOps action.VictorOpsAction.MessageType = messageType - action.VictorOpsAction.NotifyUrl = hn.Spec.VictorOpsProperties.NotifyUrl action.VictorOpsAction.UseProxy = hn.Spec.VictorOpsProperties.UseProxy return action, nil @@ -383,13 +322,15 @@ func webhookAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { return action, err } + apiToken, found := kubernetes.GetSecretForHa(hn) + var method string + if hn.Spec.WebhookProperties.Url == "" && !found { + errorList = append(errorList, "property webhookProperties.url is required") + } if hn.Spec.WebhookProperties.BodyTemplate == "" { errorList = append(errorList, "property webhookProperties.bodyTemplate is required") } - if len(hn.Spec.WebhookProperties.Headers) == 0 { - errorList = append(errorList, "property webhookProperties.headers is required") - } if hn.Spec.WebhookProperties.Method == "" { errorList = append(errorList, "property webhookProperties.method is required") } @@ -401,26 +342,38 @@ func webhookAction(hn *humiov1alpha1.HumioAction) (*humioapi.Action, error) { hn.Spec.WebhookProperties.Method, strings.Join(acceptedMethods, ", "))) } } - if _, err := url.ParseRequestURI(hn.Spec.WebhookProperties.Url); err != nil { + if hn.Spec.WebhookProperties.Url != "" { + action.WebhookAction.Url = hn.Spec.WebhookProperties.Url + } else { + action.WebhookAction.Url = apiToken + } + if _, err := url.ParseRequestURI(action.WebhookAction.Url); err != nil { errorList = append(errorList, fmt.Sprintf("invalid url for webhookProperties.url: %s", err.Error())) } + allHeaders, found := kubernetes.GetFullSetOfMergedWebhookheaders(hn) + if len(allHeaders) != len(hn.Spec.WebhookProperties.Headers)+len(hn.Spec.WebhookProperties.SecretHeaders) { + errorList = append(errorList, "webhookProperties contains duplicate keys") + } if len(errorList) > 0 { return ifErrors(action, ActionTypeWebhook, errorList) } + + if found { + action.WebhookAction.Headers = []humioapi.HttpHeaderEntryInput{} + for k, v := range allHeaders { + action.WebhookAction.Headers = append(action.WebhookAction.Headers, + humioapi.HttpHeaderEntryInput{ + Header: k, + Value: v, + }, + ) + } + } + action.Type = humioapi.ActionTypeWebhook action.WebhookAction.BodyTemplate = hn.Spec.WebhookProperties.BodyTemplate action.WebhookAction.Method = method - action.WebhookAction.Url = hn.Spec.WebhookProperties.Url action.WebhookAction.UseProxy = hn.Spec.WebhookProperties.UseProxy - action.WebhookAction.Headers = []humioapi.HttpHeaderEntryInput{} - for k, v := range hn.Spec.WebhookProperties.Headers { - action.WebhookAction.Headers = append(action.WebhookAction.Headers, - humioapi.HttpHeaderEntryInput{ - Header: k, - Value: v, - }, - ) - } return action, nil } diff --git a/pkg/humio/action_transform_test.go b/pkg/humio/action_transform_test.go index 1be6362c..fdb89174 100644 --- a/pkg/humio/action_transform_test.go +++ b/pkg/humio/action_transform_test.go @@ -88,7 +88,7 @@ func TestActionCRAsAction(t *testing.T) { }, nil, true, - fmt.Sprintf("%s failed due to errors: property slackProperties.fields is required, invalid url for slackProperties.url: parse \"\": empty url", ActionTypeSlack), + fmt.Sprintf("%s failed due to errors: property slackProperties.url is required, property slackProperties.fields is required, invalid url for slackProperties.url: parse \"\": empty url", ActionTypeSlack), }, { "missing required slackPostMessageProperties", @@ -116,7 +116,7 @@ func TestActionCRAsAction(t *testing.T) { }, nil, true, - fmt.Sprintf("%s failed due to errors: property victorOpsProperties.messageType is required, invalid url for victorOpsProperties.notifyUrl: parse \"\": empty url", ActionTypeVictorOps), + fmt.Sprintf("%s failed due to errors: property victorOpsProperties.notifyUrl is required, property victorOpsProperties.messageType is required, invalid url for victorOpsProperties.notifyUrl: parse \"\": empty url", ActionTypeVictorOps), }, { "missing required webhookProperties", @@ -130,7 +130,7 @@ func TestActionCRAsAction(t *testing.T) { }, nil, true, - fmt.Sprintf("%s failed due to errors: property webhookProperties.bodyTemplate is required, property webhookProperties.headers is required, property webhookProperties.method is required, invalid url for webhookProperties.url: parse \"\": empty url", ActionTypeWebhook), + fmt.Sprintf("%s failed due to errors: property webhookProperties.url is required, property webhookProperties.bodyTemplate is required, property webhookProperties.method is required, invalid url for webhookProperties.url: parse \"\": empty url", ActionTypeWebhook), }, { "invalid pagerDutyProperties.severity", @@ -194,6 +194,33 @@ func TestActionCRAsAction(t *testing.T) { true, "could not find action type: no properties specified for action", }, + { + "duplicate header in webhookProperties", + args{ + &humiov1alpha1.HumioAction{ + Spec: humiov1alpha1.HumioActionSpec{ + Name: "action", + WebhookProperties: &humiov1alpha1.HumioActionWebhookProperties{ + Url: "http://127.0.0.1", + Method: "POST", + BodyTemplate: "some body", + Headers: map[string]string{ + "key": "value", + }, + SecretHeaders: []humiov1alpha1.HeadersSource{ + { + Name: "key", + ValueFrom: humiov1alpha1.VarSource{}, + }, + }, + }, + }, + }, + }, + nil, + true, + fmt.Sprintf("%s failed due to errors: webhookProperties contains duplicate keys", ActionTypeWebhook), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/humio/alert_transform.go b/pkg/humio/alert_transform.go index 7a86f5a7..512542ba 100644 --- a/pkg/humio/alert_transform.go +++ b/pkg/humio/alert_transform.go @@ -51,7 +51,7 @@ func AlertHydrate(ha *humiov1alpha1.HumioAlert, alert *humioapi.Alert, actionIdM ha.ObjectMeta = metav1.ObjectMeta{ Annotations: map[string]string{ - ActionIdentifierAnnotation: alert.ID, + AlertIdentifierAnnotation: alert.ID, }, } diff --git a/pkg/kubernetes/humioaction_secret_helpers.go b/pkg/kubernetes/humioaction_secret_helpers.go new file mode 100644 index 00000000..d52d521f --- /dev/null +++ b/pkg/kubernetes/humioaction_secret_helpers.go @@ -0,0 +1,48 @@ +package kubernetes + +import ( + "fmt" + "github.com/humio/humio-operator/api/v1alpha1" +) + +var haSecrets map[string]string = make(map[string]string) +var haWebhookHeaders map[string]map[string]string = make(map[string]map[string]string) + +func GetSecretForHa(hn *v1alpha1.HumioAction) (string, bool) { + if secret, found := haSecrets[fmt.Sprintf("%s %s", hn.Namespace, hn.Name)]; found { + return secret, true + } + return "", false +} + +func StoreSingleSecretForHa(hn *v1alpha1.HumioAction, token string) { + key := fmt.Sprintf("%s %s", hn.Namespace, hn.Name) + haSecrets[key] = token +} + +func GetFullSetOfMergedWebhookheaders(hn *v1alpha1.HumioAction) (map[string]string, bool) { + if secret, found := haWebhookHeaders[fmt.Sprintf("%s %s", hn.Namespace, hn.Name)]; found { + return secret, true + } + return nil, false +} + +func StoreFullSetOfMergedWebhookActionHeaders(hn *v1alpha1.HumioAction, resolvedSecretHeaders map[string]string) { + key := fmt.Sprintf("%s %s", hn.Namespace, hn.Name) + if len(resolvedSecretHeaders) == 0 { + haWebhookHeaders[key] = hn.Spec.WebhookProperties.Headers + return + } + if hn.Spec.WebhookProperties.Headers == nil { + haWebhookHeaders[key] = resolvedSecretHeaders + return + } + mergedHeaders := make(map[string]string, len(hn.Spec.WebhookProperties.Headers)+len(resolvedSecretHeaders)) + for headerName, headerValue := range hn.Spec.WebhookProperties.Headers { + mergedHeaders[headerName] = headerValue + } + for headerName, headerValue := range resolvedSecretHeaders { + mergedHeaders[headerName] = headerValue + } + haWebhookHeaders[key] = mergedHeaders +}