diff --git a/PROJECT b/PROJECT index e400710..7186b4c 100644 --- a/PROJECT +++ b/PROJECT @@ -10,6 +10,9 @@ resources: - group: nr kind: ApmAlertCondition version: v1 +- group: nr + kind: AlertsChannel + version: v1 - group: nr kind: AlertsNrqlCondition version: v1 diff --git a/README.md b/README.md index cec2a91..fe58500 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Currently the operator supports managing the following resources: - Alert Policies - NRQL Alert Conditions. - Alert Conditions for APM, Browser and mobile +- Alert Channels # Quick Start @@ -198,6 +199,44 @@ The operator will create and update alert policies and NRQL alert conditions as region: "US" ``` + +### Create an Alerts Channel + +1. We'll be using the following [example alerts channel](/examples/example_alerts_channel.yaml) configuration file. You will need to update the [`api_key`](/examples/example_alerts_channel.yaml#6) field with your New Relic [personal API key](https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#personal-api-key).
+ + **examples/example_alerts_channel.yaml** + + ```yaml + apiVersion: nr.k8s.newrelic.com/v1 + kind: AlertsChannel + metadata: + name: my-channel1 + spec: + api_key: + # api_key_secret: + # name: nr-api-key + # namespace: default + # key_name: api-key + name: "my alert channel" + region: "US" + type: "email" + links: + # Policy links can be by NR PolicyID, NR PolicyName AND/OR K8s AlertPolicy object reference + policy_ids: + - 1 + policy_names: + - "k8s created policy" + policy_kubernetes_objects: + - name: "my-policy" + namespace: "default" + configuration: + recipients: "me@email.com" + ``` + + > **Note:** The New Relic Alerts API does not allow updating Alerts Channels. In order to change a channel, you will need to either rename the k8s AlertsChannel object to create a new one and delete the old one or manually delete the k8s AlertsChannel object and create a new one. + + + ### Uninstall the operator The Operator can be removed with the reverse of installation, namely building the kubernetes resource files with `kustomize` and running `kubectl delete` diff --git a/api/v1/alerts_apmcondition_webhook_test.go b/api/v1/alerts_apmcondition_webhook_test.go index 1e7b739..0a4b22c 100644 --- a/api/v1/alerts_apmcondition_webhook_test.go +++ b/api/v1/alerts_apmcondition_webhook_test.go @@ -64,9 +64,9 @@ var _ = Describe("alertsAPMCondition_webhook", func() { }, nil } }) + Context("ValidateCreate", func() { Context("With a valid Apm Condition", func() { - It("Should create the apm condition", func() { err := r.ValidateCreate() Expect(err).ToNot(HaveOccurred()) @@ -75,7 +75,6 @@ var _ = Describe("alertsAPMCondition_webhook", func() { }) Context("With an invalid Type", func() { - BeforeEach(func() { r.Spec.Type = "burritos" }) @@ -88,7 +87,6 @@ var _ = Describe("alertsAPMCondition_webhook", func() { }) Context("With an invalid Metric", func() { - BeforeEach(func() { r.Spec.Type = "moar burritos" }) @@ -99,8 +97,8 @@ var _ = Describe("alertsAPMCondition_webhook", func() { Expect(err.Error()).To(ContainSubstring("moar burritos")) }) }) - Context("With an invalid APMTerms", func() { + Context("With an invalid APMTerms", func() { BeforeEach(func() { r.Spec.APMTerms[0].TimeFunction = "moar burritos" r.Spec.APMTerms[0].Priority = "moar tacos" @@ -118,7 +116,6 @@ var _ = Describe("alertsAPMCondition_webhook", func() { }) Context("With an invalid userDefined type", func() { - BeforeEach(func() { r.Spec.UserDefined = alerts.ConditionUserDefined{ Metric: "Custom/foo", @@ -132,12 +129,12 @@ var _ = Describe("alertsAPMCondition_webhook", func() { Expect(err.Error()).To(ContainSubstring("invalid type")) }) }) - }) Context("ValidateUpdate", func() { Context("When deleting an existing apm Condition with a delete policy", func() { var update AlertsAPMCondition + BeforeEach(func() { currentTime := v1.Time{Time: time.Now()} //make copy of existing object to update diff --git a/api/v1/alerts_nrqlcondition_types_test.go b/api/v1/alerts_nrqlcondition_types_test.go index a5a1b80..95942f5 100644 --- a/api/v1/alerts_nrqlcondition_types_test.go +++ b/api/v1/alerts_nrqlcondition_types_test.go @@ -16,7 +16,6 @@ var _ = Describe("AlertsNrqlConditionSpec", func() { var condition AlertsNrqlConditionSpec BeforeEach(func() { - condition = AlertsNrqlConditionSpec{} condition.Enabled = true condition.ExistingPolicyID = "42" diff --git a/api/v1/alerts_policy_types.go b/api/v1/alerts_policy_types.go index 41dd79b..c92092b 100644 --- a/api/v1/alerts_policy_types.go +++ b/api/v1/alerts_policy_types.go @@ -116,6 +116,7 @@ func (p *AlertsPolicyCondition) SpecHash() uint32 { strippedAlertsPolicy.Spec.ExistingPolicyID = "" conditionTemplateSpecHasher := fnv.New32a() DeepHashObject(conditionTemplateSpecHasher, strippedAlertsPolicy) + return conditionTemplateSpecHasher.Sum32() } @@ -158,6 +159,7 @@ func (in AlertsPolicySpec) Equals(policyToCompare AlertsPolicySpec) bool { return false } } + return true } @@ -166,6 +168,7 @@ func GetAlertsConditionType(condition AlertsPolicyCondition) string { if condition.Spec.Type == "NRQL" { return "AlertsNrqlCondition" } + return "AlertsAPMCondition" } @@ -182,11 +185,13 @@ func (p *AlertsPolicyCondition) GenerateSpecFromApmConditionSpec(apmConditionSpe func (p *AlertsPolicyCondition) ReturnNrqlConditionSpec() (nrqlConditionSpec AlertsNrqlConditionSpec) { jsonString, _ := json.Marshal(p.Spec) json.Unmarshal(jsonString, &nrqlConditionSpec) //nolint + return } func (p *AlertsPolicyCondition) ReturnApmConditionSpec() (apmConditionSpec AlertsAPMConditionSpec) { jsonString, _ := json.Marshal(p.Spec) json.Unmarshal(jsonString, &apmConditionSpec) //nolint + return } diff --git a/api/v1/alerts_policy_webhook.go b/api/v1/alerts_policy_webhook.go index 886eb1d..0699a71 100644 --- a/api/v1/alerts_policy_webhook.go +++ b/api/v1/alerts_policy_webhook.go @@ -77,15 +77,17 @@ func (r *AlertsPolicy) ValidateCreate() error { if err != nil { collectedErrors.Collect(err) } - err = r.ValidateIncidentPreference() + err = r.ValidateIncidentPreference() if err != nil { collectedErrors.Collect(err) } + if len(*collectedErrors) > 0 { AlertsPolicyLog.Info("Errors encountered validating policy", "collectedErrors", collectedErrors) return collectedErrors } + return nil } @@ -126,6 +128,7 @@ func (r *AlertsPolicy) ValidateDelete() error { if err != nil { return err } + return nil } @@ -133,16 +136,17 @@ func (r *AlertsPolicy) DefaultIncidentPreference() { if r.Spec.IncidentPreference == "" { r.Spec.IncidentPreference = string(defaultAlertsPolicyIncidentPreference) } - r.Spec.IncidentPreference = strings.ToUpper(r.Spec.IncidentPreference) + r.Spec.IncidentPreference = strings.ToUpper(r.Spec.IncidentPreference) } func (r *AlertsPolicy) CheckForDuplicateConditions() error { - var conditionHashMap = make(map[uint32]bool) + for _, condition := range r.Spec.Conditions { conditionHashMap[condition.SpecHash()] = true } + if len(conditionHashMap) != len(r.Spec.Conditions) { AlertsPolicyLog.Info("duplicate conditions detected or hash collision", "conditionHash", conditionHashMap) return errors.New("duplicate conditions detected or hash collision") @@ -156,7 +160,9 @@ func (r *AlertsPolicy) ValidateIncidentPreference() error { case "PER_POLICY", "PER_CONDITION", "PER_CONDITION_AND_TARGET": return nil } + AlertsPolicyLog.Info("Incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET", "IncidentPreference value", r.Spec.IncidentPreference) + return errors.New("incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET") } @@ -164,10 +170,12 @@ func (r *AlertsPolicy) CheckForAPIKeyOrSecret() error { if r.Spec.APIKey != "" { return nil } + if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { return nil } } + return errors.New("either api_key or api_key_secret must be set") } diff --git a/api/v1/alerts_policy_webhook_test.go b/api/v1/alerts_policy_webhook_test.go index bc4a14c..5f8e720 100644 --- a/api/v1/alerts_policy_webhook_test.go +++ b/api/v1/alerts_policy_webhook_test.go @@ -86,6 +86,7 @@ var _ = Describe("AlertsPolicy_webhooks", func() { err := r.ValidateCreate() Expect(err).ToNot(HaveOccurred()) }) + AfterEach(func() { k8Client.Delete(context.Background(), secret) }) @@ -179,9 +180,8 @@ var _ = Describe("AlertsPolicy_webhooks", func() { }) Describe("Default", func() { - var ( - r AlertsPolicy - ) + var r AlertsPolicy + conditionSpec := AlertsPolicyConditionSpec{} conditionSpec.Terms = []AlertsNrqlConditionTerm{ { diff --git a/api/v1/alertschannel_types.go b/api/v1/alertschannel_types.go new file mode 100644 index 0000000..7b0561b --- /dev/null +++ b/api/v1/alertschannel_types.go @@ -0,0 +1,91 @@ +package v1 + +import ( + "encoding/json" + + "github.com/newrelic/newrelic-client-go/pkg/alerts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AlertsChannelSpec defines the desired state of AlertsChannel +type AlertsChannelSpec struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + APIKey string `json:"api_key,omitempty"` + APIKeySecret NewRelicAPIKeySecret `json:"api_key_secret,omitempty"` + Region string `json:"region,omitempty"` + Type string `json:"type,omitempty"` + Links ChannelLinks `json:"links,omitempty"` + Configuration AlertsChannelConfiguration `json:"configuration,omitempty"` +} + +// ChannelLinks - copy of alerts.ChannelLinks +type ChannelLinks struct { + PolicyIDs []int `json:"policy_ids,omitempty"` + PolicyNames []string `json:"policy_names,omitempty"` + PolicyKubernetesObjects []metav1.ObjectMeta `json:"policy_kubernetes_objects,omitempty"` +} + +// AlertsChannelStatus defines the observed state of AlertsChannel +type AlertsChannelStatus struct { + AppliedSpec *AlertsChannelSpec `json:"applied_spec"` + ChannelID int `json:"channel_id"` + AppliedPolicyIDs []int `json:"appliedPolicyIDs"` +} + +// AlertsChannelConfiguration - copy of alerts.ChannelConfiguration +type AlertsChannelConfiguration struct { + Recipients string `json:"recipients,omitempty"` + IncludeJSONAttachment string `json:"include_json_attachment,omitempty"` + AuthToken string `json:"auth_token,omitempty"` + APIKey string `json:"api_key,omitempty"` + Teams string `json:"teams,omitempty"` + Tags string `json:"tags,omitempty"` + URL string `json:"url,omitempty"` + Channel string `json:"channel,omitempty"` + Key string `json:"key,omitempty"` + RouteKey string `json:"route_key,omitempty"` + ServiceKey string `json:"service_key,omitempty"` + BaseURL string `json:"base_url,omitempty"` + AuthUsername string `json:"auth_username,omitempty"` + AuthPassword string `json:"auth_password,omitempty"` + PayloadType string `json:"payload_type,omitempty"` + Region string `json:"region,omitempty"` + UserID string `json:"user_id,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:printcolumn:name="Created",type="boolean",JSONPath=".status.created" + +// AlertsChannel is the Schema for the AlertsChannel API +type AlertsChannel struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AlertsChannelSpec `json:"spec,omitempty"` + Status AlertsChannelStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AlertsChannelList contains a list of AlertsChannel +type AlertsChannelList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AlertsChannel `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AlertsChannel{}, &AlertsChannelList{}) +} + +// APIChannel - Converts AlertsChannelSpec object to alerts.Channel +func (in AlertsChannelSpec) APIChannel() alerts.Channel { + jsonString, _ := json.Marshal(in) + + var APIChannel alerts.Channel + json.Unmarshal(jsonString, &APIChannel) //nolint + APIChannel.Links = alerts.ChannelLinks{} + + return APIChannel +} diff --git a/api/v1/alertschannel_types_test.go b/api/v1/alertschannel_types_test.go new file mode 100644 index 0000000..4ff7e4b --- /dev/null +++ b/api/v1/alertschannel_types_test.go @@ -0,0 +1,49 @@ +package v1 + +import ( + "fmt" + "reflect" + + "github.com/newrelic/newrelic-client-go/pkg/alerts" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AlertsChannelSpec", func() { + var alertsChannelSpec AlertsChannelSpec + + BeforeEach(func() { + alertsChannelSpec = AlertsChannelSpec{ + ID: 88, + Name: "my alert channel", + APIKey: "api-key", + APIKeySecret: NewRelicAPIKeySecret{}, + Region: "US", + Type: "email", + Links: ChannelLinks{ + PolicyIDs: []int{ + 1, + 2, + }, + }, + Configuration: AlertsChannelConfiguration{ + Recipients: "me@email.com", + }, + } + + }) + + Describe("APIChannel", func() { + It("converts AlertsChannelSpec object to alerts.Channel object from go client, retaining field values", func() { + apiChannel := alertsChannelSpec.APIChannel() + + Expect(fmt.Sprint(reflect.TypeOf(apiChannel))).To(Equal("alerts.Channel")) + Expect(apiChannel.ID).To(Equal(88)) + Expect(apiChannel.Type).To(Equal(alerts.ChannelTypes.Email)) + Expect(apiChannel.Name).To(Equal("my alert channel")) + apiConfiguration := apiChannel.Configuration + Expect(apiConfiguration.Recipients).To(Equal("me@email.com")) + }) + }) +}) diff --git a/api/v1/alertschannel_webhook.go b/api/v1/alertschannel_webhook.go new file mode 100644 index 0000000..39be6f5 --- /dev/null +++ b/api/v1/alertschannel_webhook.go @@ -0,0 +1,132 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "errors" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/newrelic/newrelic-client-go/pkg/alerts" + + "github.com/newrelic/newrelic-kubernetes-operator/interfaces" +) + +// log is for logging in this package. +var ( + alertschannellog = logf.Log.WithName("alertschannel-resource") +) + +// SetupWebhookWithManager - instantiates the Webhook +func (r *AlertsChannel) SetupWebhookWithManager(mgr ctrl.Manager) error { + alertClientFunc = interfaces.InitializeAlertsClient + k8Client = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-nr-k8s-newrelic-com-v1-alertschannel,mutating=true,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertschannels,verbs=create;update,versions=v1,name=malertschannel.kb.io,sideEffects=None + +var _ webhook.Defaulter = &AlertsChannel{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *AlertsChannel) Default() { + alertschannellog.Info("default", "name", r.Name) + + if r.Status.AppliedSpec == nil { + log.Info("Setting null Applied Spec to empty interface") + r.Status.AppliedSpec = &AlertsChannelSpec{} + } + + if r.Status.AppliedPolicyIDs == nil { + log.Info("Setting null AppliedPolicyIDs to empty interface") + r.Status.AppliedPolicyIDs = []int{} + } +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-nr-k8s-newrelic-com-v1-alertschannel,mutating=false,failurePolicy=fail,groups=nr.k8s.newrelic.com,resources=alertschannels,versions=v1,name=valertschannel.kb.io,sideEffects=None + +var _ webhook.Validator = &AlertsChannel{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *AlertsChannel) ValidateCreate() error { + alertschannellog.Info("validate create", "name", r.Name) + + return r.ValidateAlertsChannel() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *AlertsChannel) ValidateUpdate(old runtime.Object) error { + alertschannellog.Info("validate update", "name", r) + + return r.ValidateAlertsChannel() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *AlertsChannel) ValidateDelete() error { + alertschannellog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil +} + +// ValidateAlertsChannel - Validates create/update of AlertsChannel +func (r *AlertsChannel) ValidateAlertsChannel() error { + err := CheckForAPIKeyOrSecret(r.Spec.APIKey, r.Spec.APIKeySecret) + if err != nil { + return err + } + + if !ValidRegion(r.Spec.Region) { + return errors.New("Invalid region set, value was: " + r.Spec.Region) + } + + var invalidAttributes InvalidAttributeSlice + + r.ValidateType() + invalidAttributes = append(invalidAttributes, r.ValidateType()...) + + if len(invalidAttributes) > 0 { + return errors.New("error with invalid attributes: \n" + invalidAttributes.errorString()) + } + + return nil +} + +//ValidateType - Validates the Type attribute +func (r *AlertsChannel) ValidateType() InvalidAttributeSlice { + switch r.Spec.Type { + case string(alerts.ChannelTypes.Email), + string(alerts.ChannelTypes.OpsGenie), + string(alerts.ChannelTypes.PagerDuty), + string(alerts.ChannelTypes.Slack), + string(alerts.ChannelTypes.User), + string(alerts.ChannelTypes.VictorOps), + string(alerts.ChannelTypes.Webhook): + + return []invalidAttribute{} + default: + alertschannellog.Info("Invalid Type attribute", "Type", r.Spec.Type) + + return []invalidAttribute{{attribute: "Type", value: r.Spec.Type}} + } +} diff --git a/api/v1/alertschannel_webhook_test.go b/api/v1/alertschannel_webhook_test.go new file mode 100644 index 0000000..bbb10cd --- /dev/null +++ b/api/v1/alertschannel_webhook_test.go @@ -0,0 +1,100 @@ +package v1 + +import ( + "github.com/newrelic/newrelic-client-go/pkg/alerts" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/newrelic/newrelic-kubernetes-operator/interfaces" + "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" +) + +var _ = Describe("AlertsChannel_webhook", func() { + var ( + r AlertsChannel + alertsClient *interfacesfakes.FakeNewRelicAlertsClient + ) + + BeforeEach(func() { + k8Client = testk8sClient + alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} + fakeAlertFunc := func(string, string) (interfaces.NewRelicAlertsClient, error) { + return alertsClient, nil + } + alertClientFunc = fakeAlertFunc + r = AlertsChannel{ + ObjectMeta: v1.ObjectMeta{ + Name: "test alert channel", + }, + Spec: AlertsChannelSpec{ + ID: 88, + Name: "my alert channel", + APIKey: "api-key", + APIKeySecret: NewRelicAPIKeySecret{}, + Region: "US", + Type: "email", + Links: ChannelLinks{ + PolicyIDs: []int{ + 1, + 2, + }, + }, + Configuration: AlertsChannelConfiguration{ + Recipients: "me@email.com", + }, + }, + } + + alertsClient.GetPolicyStub = func(int) (*alerts.Policy, error) { + return &alerts.Policy{ + ID: 46286, + }, nil + } + }) + + Context("ValidateCreate", func() { + Context("With a valid Alert Channel", func() { + It("Should create the Alert Channel", func() { + err := r.ValidateCreate() + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("With an invalid Region", func() { + BeforeEach(func() { + r.Spec.Region = "hamburgers" + }) + + It("Should reject the Alert Channel creation", func() { + err := r.ValidateCreate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("hamburgers")) + }) + }) + + Context("With an invalid Type", func() { + BeforeEach(func() { + r.Spec.Type = "burritos" + }) + + It("Should reject the Alert Channel creation", func() { + err := r.ValidateCreate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("burritos")) + }) + }) + + Context("With no API Key or secret", func() { + BeforeEach(func() { + r.Spec.APIKey = "" + }) + + It("Should reject the Alert Channel creation", func() { + err := r.ValidateCreate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("either api_key or api_key_secret must be set")) + }) + }) + }) +}) diff --git a/api/v1/apmalertcondition_webhook.go b/api/v1/apmalertcondition_webhook.go index 9e85f09..2a63b67 100644 --- a/api/v1/apmalertcondition_webhook.go +++ b/api/v1/apmalertcondition_webhook.go @@ -132,6 +132,7 @@ func (r *ApmAlertCondition) ValidateUpdate(old runtime.Object) error { if len(invalidAttributes) > 0 { return errors.New("error with invalid attributes") } + return r.CheckExistingPolicyID() } @@ -150,9 +151,11 @@ func (r *ApmAlertCondition) ValidateType() InvalidAttributeSlice { string(alerts.ConditionTypes.BrowserMetric), string(alerts.ConditionTypes.MobileMetric), string(alerts.ConditionTypes.ServersMetric): + return []invalidAttribute{} default: log.Info("Invalid Type attribute", "Type", r.Spec.Type) + return []invalidAttribute{{attribute: "Type", value: r.Spec.Type}} } } @@ -192,15 +195,18 @@ func (r *ApmAlertCondition) ValidateMetric() InvalidAttributeSlice { alerts.MetricTypes.UserDefined, alerts.MetricTypes.ViewLoading, alerts.MetricTypes.WebApplication: + return []invalidAttribute{} default: log.Info("Invalid Metric attribute", "Metric", r.Spec.Metric) + return []invalidAttribute{{attribute: "Type", value: r.Spec.Metric}} } } func (r *ApmAlertCondition) ValidateTerms() InvalidAttributeSlice { var invalidTerms InvalidAttributeSlice + for _, term := range r.Spec.Terms { switch alerts.TimeFunctionType(term.TimeFunction) { case alerts.TimeFunctionTypes.All, alerts.TimeFunctionTypes.Any: @@ -212,6 +218,7 @@ func (r *ApmAlertCondition) ValidateTerms() InvalidAttributeSlice { value: term.TimeFunction, }) } + switch alerts.OperatorType(term.Operator) { case alerts.OperatorTypes.Equal, alerts.OperatorTypes.Above, alerts.OperatorTypes.Below: continue @@ -222,6 +229,7 @@ func (r *ApmAlertCondition) ValidateTerms() InvalidAttributeSlice { value: term.Operator, }) } + switch alerts.PriorityType(term.Priority) { case alerts.PriorityTypes.Critical, alerts.PriorityTypes.Warning: continue @@ -246,9 +254,11 @@ func (r *ApmAlertCondition) ValidateUserDefinedValueFunction() InvalidAttributeS alerts.ValueFunctionTypes.SampleSize, alerts.ValueFunctionTypes.SingleValue, alerts.ValueFunctionTypes.Total: + return []invalidAttribute{} default: log.Info("Invalid UserDefined.ValueFunction passed", "UserDefined.ValueFunction", r.Spec.UserDefined.ValueFunction) + return []invalidAttribute{{attribute: "UserDefined.ValueFunction: ", value: string(r.Spec.UserDefined.ValueFunction)}} } } @@ -257,16 +267,18 @@ func (r *ApmAlertCondition) CheckExistingPolicyID() error { log.Info("Checking existing", "policyId", r.Spec.ExistingPolicyID) ctx := context.Background() var apiKey string + if r.Spec.APIKey == "" { key := types.NamespacedName{Namespace: r.Spec.APIKeySecret.Namespace, Name: r.Spec.APIKeySecret.Name} var apiKeySecret v1.Secret + getErr := k8Client.Get(ctx, key, &apiKeySecret) if getErr != nil { log.Error(getErr, "Error getting secret") return getErr } - apiKey = string(apiKeySecret.Data[r.Spec.APIKeySecret.KeyName]) + apiKey = string(apiKeySecret.Data[r.Spec.APIKeySecret.KeyName]) } else { apiKey = r.Spec.APIKey } @@ -278,28 +290,35 @@ func (r *ApmAlertCondition) CheckExistingPolicyID() error { "API Key", interfaces.PartialAPIKey(apiKey), "region", r.Spec.Region, ) + return errAlertClient } + alertPolicy, errAlertPolicy := alertsClient.GetPolicy(r.Spec.ExistingPolicyID) if errAlertPolicy != nil { if r.GetDeletionTimestamp() != nil { log.Info("Deleting resource", "errAlertPolicy", errAlertPolicy) if strings.Contains(errAlertPolicy.Error(), "no alert policy found for id") { log.Info("ExistingAlertPolicy not found but we are deleting the condition so this is ok") + return nil } } + log.Error(errAlertPolicy, "failed to get policy", "policyId", r.Spec.ExistingPolicyID, "API Key", interfaces.PartialAPIKey(apiKey), "region", r.Spec.Region, ) + return errAlertPolicy } if alertPolicy.ID != r.Spec.ExistingPolicyID { log.Info("Alert policy returned by the API failed to match provided policy ID") + return errors.New("alert policy returned by API did not match") } + return nil } @@ -307,25 +326,30 @@ func (r *ApmAlertCondition) CheckForAPIKeyOrSecret() error { if r.Spec.APIKey != "" { return nil } + if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { return nil } } + return errors.New("either api_key or api_key_secret must be set") } func (r *ApmAlertCondition) CheckRequiredFields() error { - missingFields := []string{} + if r.Spec.Region == "" { missingFields = append(missingFields, "region") } + if r.Spec.ExistingPolicyID == 0 { missingFields = append(missingFields, "existing_policy_id") } + if len(missingFields) > 0 { return errors.New(strings.Join(missingFields, " and ") + " must be set") } + return nil } diff --git a/api/v1/apmalertcondition_webhook_test.go b/api/v1/apmalertcondition_webhook_test.go index 66ceb03..07b00d2 100644 --- a/api/v1/apmalertcondition_webhook_test.go +++ b/api/v1/apmalertcondition_webhook_test.go @@ -66,16 +66,13 @@ var _ = Describe("apmAlertCondition_webhook", func() { }) Context("ValidateCreate", func() { Context("With a valid Apm Condition", func() { - It("Should create the apm condition", func() { err := r.ValidateCreate() Expect(err).ToNot(HaveOccurred()) - }) }) Context("With an invalid Type", func() { - BeforeEach(func() { r.Spec.Type = "burritos" }) @@ -88,7 +85,6 @@ var _ = Describe("apmAlertCondition_webhook", func() { }) Context("With an invalid Metric", func() { - BeforeEach(func() { r.Spec.Type = "moar burritos" }) @@ -99,13 +95,12 @@ var _ = Describe("apmAlertCondition_webhook", func() { Expect(err.Error()).To(ContainSubstring("moar burritos")) }) }) - Context("With an invalid Terms", func() { + Context("With an invalid Terms", func() { BeforeEach(func() { r.Spec.Terms[0].TimeFunction = "moar burritos" r.Spec.Terms[0].Priority = "moar tacos" r.Spec.Terms[0].Operator = "moar hamburgers" - }) It("Should reject the apm condition creation", func() { @@ -118,7 +113,6 @@ var _ = Describe("apmAlertCondition_webhook", func() { }) Context("With an invalid userDefined type", func() { - BeforeEach(func() { r.Spec.UserDefined = alerts.ConditionUserDefined{ Metric: "Custom/foo", @@ -155,5 +149,4 @@ var _ = Describe("apmAlertCondition_webhook", func() { }) }) }) - }) diff --git a/api/v1/common.go b/api/v1/common.go index 884aec1..473d7da 100644 --- a/api/v1/common.go +++ b/api/v1/common.go @@ -1,9 +1,11 @@ package v1 import ( + "errors" "hash" "github.com/davecgh/go-spew/spew" + "github.com/newrelic/newrelic-client-go/pkg/region" ) // DeepHashObject writes specified object to hash using the spew library @@ -35,3 +37,30 @@ type NewRelicAPIKeySecret struct { Namespace string `json:"namespace,omitempty"` KeyName string `json:"key_name,omitempty"` } + +//ValidRegion - returns true if a valid region is passed +func ValidRegion(input string) bool { + _, err := region.Parse(input) + if err != nil { + return false + } else if input == "" { + return false + } + + return true +} + +//CheckForAPIKeyOrSecret - returns error if a API KEY or k8 secret is not passed in +func CheckForAPIKeyOrSecret(apiKey string, secret NewRelicAPIKeySecret) error { + if apiKey != "" { + return nil + } + + if secret != (NewRelicAPIKeySecret{}) { + if secret.Name != "" && secret.Namespace != "" && secret.KeyName != "" { + return nil + } + } + + return errors.New("either api_key or api_key_secret must be set") +} diff --git a/api/v1/nrqlalertcondition_types.go b/api/v1/nrqlalertcondition_types.go index 19611fd..6ffd469 100644 --- a/api/v1/nrqlalertcondition_types.go +++ b/api/v1/nrqlalertcondition_types.go @@ -76,5 +76,6 @@ func (in NrqlAlertConditionSpec) APICondition() alerts.NrqlCondition { jsonString, _ := json.Marshal(in) var APICondition alerts.NrqlCondition json.Unmarshal(jsonString, &APICondition) //nolint + return APICondition } diff --git a/api/v1/nrqlalertcondition_webhook.go b/api/v1/nrqlalertcondition_webhook.go index f4d9d9e..27846ed 100644 --- a/api/v1/nrqlalertcondition_webhook.go +++ b/api/v1/nrqlalertcondition_webhook.go @@ -77,6 +77,7 @@ func (r *NrqlAlertCondition) ValidateCreate() error { if err != nil { return err } + return r.CheckExistingPolicyID() } @@ -92,6 +93,7 @@ func (r *NrqlAlertCondition) ValidateUpdate(old runtime.Object) error { if err != nil { return err } + return r.CheckExistingPolicyID() } @@ -107,6 +109,7 @@ func (r *NrqlAlertCondition) CheckExistingPolicyID() error { log.Info("Checking existing", "policyId", r.Spec.ExistingPolicyID) ctx := context.Background() var apiKey string + if r.Spec.APIKey == "" { key := types.NamespacedName{Namespace: r.Spec.APIKeySecret.Namespace, Name: r.Spec.APIKeySecret.Name} var apiKeySecret v1.Secret @@ -116,7 +119,6 @@ func (r *NrqlAlertCondition) CheckExistingPolicyID() error { return getErr } apiKey = string(apiKeySecret.Data[r.Spec.APIKeySecret.KeyName]) - } else { apiKey = r.Spec.APIKey } @@ -130,6 +132,7 @@ func (r *NrqlAlertCondition) CheckExistingPolicyID() error { ) return errAlertClient } + alertPolicy, errAlertPolicy := alertsClient.GetPolicy(r.Spec.ExistingPolicyID) if errAlertPolicy != nil { if r.GetDeletionTimestamp() != nil { @@ -146,10 +149,12 @@ func (r *NrqlAlertCondition) CheckExistingPolicyID() error { ) return errAlertPolicy } + if alertPolicy.ID != r.Spec.ExistingPolicyID { log.Info("Alert policy returned by the API failed to match provided policy ID") return errors.New("alert policy returned by API did not match") } + return nil } @@ -157,25 +162,30 @@ func (r *NrqlAlertCondition) CheckForAPIKeyOrSecret() error { if r.Spec.APIKey != "" { return nil } + if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { return nil } } + return errors.New("either api_key or api_key_secret must be set") } func (r *NrqlAlertCondition) CheckRequiredFields() error { - missingFields := []string{} + if r.Spec.Region == "" { missingFields = append(missingFields, "region") } + if r.Spec.ExistingPolicyID == 0 { missingFields = append(missingFields, "existing_policy_id") } + if len(missingFields) > 0 { return errors.New(strings.Join(missingFields, " and ") + " must be set") } + return nil } diff --git a/api/v1/nrqlalertcondition_webhook_test.go b/api/v1/nrqlalertcondition_webhook_test.go index f906057..94be0cb 100644 --- a/api/v1/nrqlalertcondition_webhook_test.go +++ b/api/v1/nrqlalertcondition_webhook_test.go @@ -170,8 +170,6 @@ var _ = Describe("ValidateCreate", func() { }) Describe("CheckExistingPolicyID", func() { - BeforeEach(func() {}) - Context("With a valid API Key", func() { BeforeEach(func() {}) @@ -213,6 +211,7 @@ var _ = Describe("ValidateCreate", func() { Context("ValidateUpdate", func() { Context("When deleting an existing nrql Condition with a delete policy", func() { var update NrqlAlertCondition + BeforeEach(func() { currentTime := metav1.Time{Time: time.Now()} //make copy of existing object to update diff --git a/api/v1/policy_types.go b/api/v1/policy_types.go index c910623..d7d2616 100644 --- a/api/v1/policy_types.go +++ b/api/v1/policy_types.go @@ -102,6 +102,7 @@ func (p *PolicyCondition) SpecHash() uint32 { strippedPolicy.Spec.ExistingPolicyID = 0 conditionTemplateSpecHasher := fnv.New32a() DeepHashObject(conditionTemplateSpecHasher, strippedPolicy) + return conditionTemplateSpecHasher.Sum32() } @@ -117,18 +118,23 @@ func (in PolicySpec) Equals(policyToCompare PolicySpec) bool { if in.IncidentPreference != policyToCompare.IncidentPreference { return false } + if in.Name != policyToCompare.Name { return false } + if in.APIKey != policyToCompare.APIKey { return false } + if in.Region != policyToCompare.Region { return false } + if in.APIKeySecret != policyToCompare.APIKeySecret { return false } + if len(in.Conditions) != len(policyToCompare.Conditions) { return false } @@ -144,6 +150,7 @@ func (in PolicySpec) Equals(policyToCompare PolicySpec) bool { return false } } + return true } @@ -152,8 +159,8 @@ func GetConditionType(condition PolicyCondition) string { if condition.Spec.Type == "NRQL" { return "NrqlAlertCondition" } - return "ApmAlertCondition" + return "ApmAlertCondition" } func (p *PolicyCondition) GenerateSpecFromNrqlConditionSpec(nrqlConditionSpec NrqlAlertConditionSpec) { @@ -169,11 +176,13 @@ func (p *PolicyCondition) GenerateSpecFromApmConditionSpec(apmConditionSpec ApmA func (p *PolicyCondition) ReturnNrqlConditionSpec() (nrqlAlertConditionSpec NrqlAlertConditionSpec) { jsonString, _ := json.Marshal(p.Spec) json.Unmarshal(jsonString, &nrqlAlertConditionSpec) //nolint + return } func (p *PolicyCondition) ReturnApmConditionSpec() (apmAlertConditionSpec ApmAlertConditionSpec) { jsonString, _ := json.Marshal(p.Spec) json.Unmarshal(jsonString, &apmAlertConditionSpec) //nolint + return } diff --git a/api/v1/policy_types_test.go b/api/v1/policy_types_test.go index e525337..20204f6 100644 --- a/api/v1/policy_types_test.go +++ b/api/v1/policy_types_test.go @@ -295,6 +295,7 @@ var _ = Describe("GetNrqlConditionSpec", func() { APMSpecificSpec{}, }, } + It("Should return a matching NrqlConditionSpec", func() { nrqlConditionSpec := condition.ReturnNrqlConditionSpec() Expect(nrqlConditionSpec.Type).To(Equal("NRQL")) @@ -363,6 +364,7 @@ var _ = Describe("GetApmConditionSpec", func() { }, }, } + It("Should return a matching ApmConditionSpec", func() { apmConditionSpec := condition.ReturnApmConditionSpec() Expect(apmConditionSpec.Type).To(Equal("apm_app_metric")) diff --git a/api/v1/policy_webhook.go b/api/v1/policy_webhook.go index f063da4..1d93178 100644 --- a/api/v1/policy_webhook.go +++ b/api/v1/policy_webhook.go @@ -64,8 +64,8 @@ func (r *Policy) ValidateCreate() error { Log.Info("validate create", "name", r.Name) collectedErrors := new(customErrors.ErrorCollector) - err := r.CheckForAPIKeyOrSecret() + err := r.CheckForAPIKeyOrSecret() if err != nil { collectedErrors.Collect(err) } @@ -74,15 +74,17 @@ func (r *Policy) ValidateCreate() error { if err != nil { collectedErrors.Collect(err) } - err = r.ValidateIncidentPreference() + err = r.ValidateIncidentPreference() if err != nil { collectedErrors.Collect(err) } + if len(*collectedErrors) > 0 { Log.Info("Errors encountered validating policy", "collectedErrors", collectedErrors) return collectedErrors } + return nil } @@ -123,6 +125,7 @@ func (r *Policy) ValidateDelete() error { if err != nil { return err } + return nil } @@ -130,18 +133,20 @@ func (r *Policy) DefaultIncidentPreference() { if r.Spec.IncidentPreference == "" { r.Spec.IncidentPreference = defaultPolicyIncidentPreference } - r.Spec.IncidentPreference = strings.ToUpper(r.Spec.IncidentPreference) + r.Spec.IncidentPreference = strings.ToUpper(r.Spec.IncidentPreference) } func (r *Policy) CheckForDuplicateConditions() error { - var conditionHashMap = make(map[uint32]bool) + for _, condition := range r.Spec.Conditions { conditionHashMap[condition.SpecHash()] = true } + if len(conditionHashMap) != len(r.Spec.Conditions) { log.Info("duplicate conditions detected or hash collision", "conditionHash", conditionHashMap) + return errors.New("duplicate conditions detected or hash collision") } @@ -153,7 +158,9 @@ func (r *Policy) ValidateIncidentPreference() error { case "PER_POLICY", "PER_CONDITION", "PER_CONDITION_AND_TARGET": return nil } + log.Info("Incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET", "IncidentPreference value", r.Spec.IncidentPreference) + return errors.New("incident preference must be PER_POLICY, PER_CONDITION, or PER_CONDITION_AND_TARGET") } @@ -161,10 +168,12 @@ func (r *Policy) CheckForAPIKeyOrSecret() error { if r.Spec.APIKey != "" { return nil } + if r.Spec.APIKeySecret != (NewRelicAPIKeySecret{}) { if r.Spec.APIKeySecret.Name != "" && r.Spec.APIKeySecret.Namespace != "" && r.Spec.APIKeySecret.KeyName != "" { return nil } } + return errors.New("either api_key or api_key_secret must be set") } diff --git a/api/v1/policy_webhook_test.go b/api/v1/policy_webhook_test.go index ef2fc30..be2f131 100644 --- a/api/v1/policy_webhook_test.go +++ b/api/v1/policy_webhook_test.go @@ -26,6 +26,7 @@ var _ = Describe("Policy_webhooks", func() { })) Expect(err).ToNot(HaveOccurred()) }) + Describe("validateCreate", func() { var ( r Policy @@ -96,6 +97,7 @@ var _ = Describe("Policy_webhooks", func() { err := r.ValidateCreate() Expect(err).ToNot(HaveOccurred()) }) + AfterEach(func() { k8Client.Delete(ctx, secret) }) @@ -203,48 +205,49 @@ var _ = Describe("Policy_webhooks", func() { }) Describe("Default", func() { - var ( - r Policy - ) - r = Policy{ - Spec: PolicySpec{ - Name: "Test Policy", - IncidentPreference: "PER_POLICY", - APIKey: "api-key", - Conditions: []PolicyCondition{ - { - Spec: ConditionSpec{ - GenericConditionSpec{ - Terms: []AlertConditionTerm{ - { - Duration: "30", - Operator: "above", - Priority: "critical", - Threshold: "5", - TimeFunction: "all", + var r Policy + + BeforeEach(func() { + r = Policy{ + Spec: PolicySpec{ + Name: "Test Policy", + IncidentPreference: "PER_POLICY", + APIKey: "api-key", + Conditions: []PolicyCondition{ + { + Spec: ConditionSpec{ + GenericConditionSpec{ + Terms: []AlertConditionTerm{ + { + Duration: "30", + Operator: "above", + Priority: "critical", + Threshold: "5", + TimeFunction: "all", + }, }, + Type: "NRQL", + Name: "NRQL Condition", + RunbookURL: "http://test.com/runbook", + Enabled: true, }, - Type: "NRQL", - Name: "NRQL Condition", - RunbookURL: "http://test.com/runbook", - Enabled: true, - }, - NrqlSpecificSpec{ - Nrql: NrqlQuery{ - Query: "SELECT 1 FROM MyEvents", - SinceValue: "5", + NrqlSpecificSpec{ + Nrql: NrqlQuery{ + Query: "SELECT 1 FROM MyEvents", + SinceValue: "5", + }, + ValueFunction: "max", + ViolationCloseTimer: 60, + ExpectedGroups: 2, + IgnoreOverlap: true, }, - ValueFunction: "max", - ViolationCloseTimer: 60, - ExpectedGroups: 2, - IgnoreOverlap: true, + APMSpecificSpec{}, }, - APMSpecificSpec{}, }, }, }, - }, - } + } + }) Context("when given a policy with no incident_preference set", func() { It("should set default value of PER_POLICY", func() { diff --git a/api/v1/suite_test.go b/api/v1/suite_test.go index 931142b..edc1800 100644 --- a/api/v1/suite_test.go +++ b/api/v1/suite_test.go @@ -66,5 +66,6 @@ func ignoreAlreadyExists(err error) error { if apierrors.IsAlreadyExists(err) { return nil } + return err } diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 19cad79..f84d254 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ package v1 import ( "github.com/newrelic/newrelic-client-go/pkg/alerts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -177,6 +178,123 @@ func (in *AlertsAPMSpecificSpec) DeepCopy() *AlertsAPMSpecificSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertsChannel) DeepCopyInto(out *AlertsChannel) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertsChannel. +func (in *AlertsChannel) DeepCopy() *AlertsChannel { + if in == nil { + return nil + } + out := new(AlertsChannel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AlertsChannel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertsChannelConfiguration) DeepCopyInto(out *AlertsChannelConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertsChannelConfiguration. +func (in *AlertsChannelConfiguration) DeepCopy() *AlertsChannelConfiguration { + if in == nil { + return nil + } + out := new(AlertsChannelConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertsChannelList) DeepCopyInto(out *AlertsChannelList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AlertsChannel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertsChannelList. +func (in *AlertsChannelList) DeepCopy() *AlertsChannelList { + if in == nil { + return nil + } + out := new(AlertsChannelList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AlertsChannelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertsChannelSpec) DeepCopyInto(out *AlertsChannelSpec) { + *out = *in + out.APIKeySecret = in.APIKeySecret + in.Links.DeepCopyInto(&out.Links) + out.Configuration = in.Configuration +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertsChannelSpec. +func (in *AlertsChannelSpec) DeepCopy() *AlertsChannelSpec { + if in == nil { + return nil + } + out := new(AlertsChannelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertsChannelStatus) DeepCopyInto(out *AlertsChannelStatus) { + *out = *in + if in.AppliedSpec != nil { + in, out := &in.AppliedSpec, &out.AppliedSpec + *out = new(AlertsChannelSpec) + (*in).DeepCopyInto(*out) + } + if in.AppliedPolicyIDs != nil { + in, out := &in.AppliedPolicyIDs, &out.AppliedPolicyIDs + *out = make([]int, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertsChannelStatus. +func (in *AlertsChannelStatus) DeepCopy() *AlertsChannelStatus { + if in == nil { + return nil + } + out := new(AlertsChannelStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AlertsGenericConditionSpec) DeepCopyInto(out *AlertsGenericConditionSpec) { *out = *in @@ -567,6 +685,38 @@ func (in *ApmAlertConditionStatus) DeepCopy() *ApmAlertConditionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelLinks) DeepCopyInto(out *ChannelLinks) { + *out = *in + if in.PolicyIDs != nil { + in, out := &in.PolicyIDs, &out.PolicyIDs + *out = make([]int, len(*in)) + copy(*out, *in) + } + if in.PolicyNames != nil { + in, out := &in.PolicyNames, &out.PolicyNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PolicyKubernetesObjects != nil { + in, out := &in.PolicyKubernetesObjects, &out.PolicyKubernetesObjects + *out = make([]metav1.ObjectMeta, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelLinks. +func (in *ChannelLinks) DeepCopy() *ChannelLinks { + if in == nil { + return nil + } + out := new(ChannelLinks) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConditionSpec) DeepCopyInto(out *ConditionSpec) { *out = *in diff --git a/cmd/manager/alerts.go b/cmd/manager/alerts.go index a65580f..265352c 100644 --- a/cmd/manager/alerts.go +++ b/cmd/manager/alerts.go @@ -123,6 +123,25 @@ func registerAlerts(mgr *ctrl.Manager) error { os.Exit(1) } + //alertsChannel + alertsChannelReconciler := &controllers.AlertsChannelReconciler{ + Client: (*mgr).GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("alertsChannel"), + Scheme: (*mgr).GetScheme(), + AlertClientFunc: interfaces.InitializeAlertsClient, + } + + if err := alertsChannelReconciler.SetupWithManager(*mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "alertsChannel") + os.Exit(1) + } + + alertsChannel := &nrv1.AlertsChannel{} + if err := alertsChannel.SetupWebhookWithManager(*mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AlertsChannel") + os.Exit(1) + } + // alertspolicy alertsPolicyReconciler := &controllers.AlertsPolicyReconciler{ Client: (*mgr).GetClient(), @@ -130,7 +149,6 @@ func registerAlerts(mgr *ctrl.Manager) error { Scheme: (*mgr).GetScheme(), AlertClientFunc: interfaces.InitializeAlertsClient, } - if err := alertsPolicyReconciler.SetupWithManager(*mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AlertsPolicy") os.Exit(1) diff --git a/configs/crd/bases/nr.k8s.newrelic.com_alertschannels.yaml b/configs/crd/bases/nr.k8s.newrelic.com_alertschannels.yaml new file mode 100644 index 0000000..c1a8853 --- /dev/null +++ b/configs/crd/bases/nr.k8s.newrelic.com_alertschannels.yaml @@ -0,0 +1,218 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.3.0 + creationTimestamp: null + name: alertschannels.nr.k8s.newrelic.com +spec: + additionalPrinterColumns: + - JSONPath: .status.created + name: Created + type: boolean + group: nr.k8s.newrelic.com + names: + kind: AlertsChannel + listKind: AlertsChannelList + plural: alertschannels + singular: alertschannel + scope: Namespaced + subresources: {} + validation: + openAPIV3Schema: + description: AlertsChannel is the Schema for the AlertsChannel API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AlertsChannelSpec defines the desired state of AlertsChannel + properties: + api_key: + type: string + api_key_secret: + properties: + key_name: + type: string + name: + type: string + namespace: + type: string + type: object + configuration: + description: AlertsChannelConfiguration - copy of alerts.ChannelConfiguration + properties: + api_key: + type: string + auth_password: + type: string + auth_token: + type: string + auth_username: + type: string + base_url: + type: string + channel: + type: string + include_json_attachment: + type: string + key: + type: string + payload_type: + type: string + recipients: + type: string + region: + type: string + route_key: + type: string + service_key: + type: string + tags: + type: string + teams: + type: string + url: + type: string + user_id: + type: string + type: object + id: + type: integer + links: + description: ChannelLinks - copy of alerts.ChannelLinks + properties: + policy_ids: + items: + type: integer + type: array + policy_kubernetes_objects: + items: + type: object + type: array + policy_names: + items: + type: string + type: array + type: object + name: + type: string + region: + type: string + type: + type: string + required: + - name + type: object + status: + description: AlertsChannelStatus defines the observed state of AlertsChannel + properties: + applied_spec: + description: AlertsChannelSpec defines the desired state of AlertsChannel + properties: + api_key: + type: string + api_key_secret: + properties: + key_name: + type: string + name: + type: string + namespace: + type: string + type: object + configuration: + description: AlertsChannelConfiguration - copy of alerts.ChannelConfiguration + properties: + api_key: + type: string + auth_password: + type: string + auth_token: + type: string + auth_username: + type: string + base_url: + type: string + channel: + type: string + include_json_attachment: + type: string + key: + type: string + payload_type: + type: string + recipients: + type: string + region: + type: string + route_key: + type: string + service_key: + type: string + tags: + type: string + teams: + type: string + url: + type: string + user_id: + type: string + type: object + id: + type: integer + links: + description: ChannelLinks - copy of alerts.ChannelLinks + properties: + policy_ids: + items: + type: integer + type: array + policy_kubernetes_objects: + items: + type: object + type: array + policy_names: + items: + type: string + type: array + type: object + name: + type: string + region: + type: string + type: + type: string + required: + - name + type: object + appliedPolicyIDs: + items: + type: integer + type: array + channel_id: + type: integer + required: + - appliedPolicyIDs + - applied_spec + - channel_id + type: object + type: object + version: v1 + versions: + - name: v1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/configs/crd/kustomization.yaml b/configs/crd/kustomization.yaml index 77592e7..505c94e 100644 --- a/configs/crd/kustomization.yaml +++ b/configs/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/nr.k8s.newrelic.com_nrqlalertconditions.yaml - bases/nr.k8s.newrelic.com_policies.yaml - bases/nr.k8s.newrelic.com_apmalertconditions.yaml +- bases/nr.k8s.newrelic.com_alertschannels.yaml - bases/nr.k8s.newrelic.com_alertsnrqlconditions.yaml - bases/nr.k8s.newrelic.com_alertspolicies.yaml - bases/nr.k8s.newrelic.com_alertsapmconditions.yaml diff --git a/configs/crd/patches/cainjection_in_alertschannels.yaml b/configs/crd/patches/cainjection_in_alertschannels.yaml new file mode 100644 index 0000000..ccc8ce2 --- /dev/null +++ b/configs/crd/patches/cainjection_in_alertschannels.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: alertschannels.nr.k8s.newrelic.com diff --git a/configs/crd/patches/webhook_in_alertschannels.yaml b/configs/crd/patches/webhook_in_alertschannels.yaml new file mode 100644 index 0000000..0c45cef --- /dev/null +++ b/configs/crd/patches/webhook_in_alertschannels.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: alertschannels.nr.k8s.newrelic.com +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/configs/manifests.yaml b/configs/manifests.yaml index af01720..f04ff75 100644 --- a/configs/manifests.yaml +++ b/configs/manifests.yaml @@ -63,6 +63,25 @@ webhooks: resources: - alertspolicies sideEffects: None +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-alertschannel + failurePolicy: Fail + name: malertschannel.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertschannels + sideEffects: None - clientConfig: caBundle: Cg== service: @@ -185,6 +204,25 @@ webhooks: resources: - alertspolicies sideEffects: None +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-alertschannel + failurePolicy: Fail + name: valertschannel.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertschannels + sideEffects: None - clientConfig: caBundle: Cg== service: diff --git a/configs/rbac/alertschannel_editor_role.yaml b/configs/rbac/alertschannel_editor_role.yaml new file mode 100644 index 0000000..c7fb552 --- /dev/null +++ b/configs/rbac/alertschannel_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit alertschannels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: alertschannel-editor-role +rules: +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannels/status + verbs: + - get diff --git a/configs/rbac/alertschannel_viewer_role.yaml b/configs/rbac/alertschannel_viewer_role.yaml new file mode 100644 index 0000000..8ca0a0e --- /dev/null +++ b/configs/rbac/alertschannel_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view alertschannels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: alertschannel-viewer-role +rules: +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannels + verbs: + - get + - list + - watch +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannels/status + verbs: + - get diff --git a/configs/rbac/role.yaml b/configs/rbac/role.yaml index 404e734..d23d3d6 100644 --- a/configs/rbac/role.yaml +++ b/configs/rbac/role.yaml @@ -69,6 +69,26 @@ rules: - get - patch - update +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannels/status + verbs: + - get + - patch + - update - apiGroups: - nr.k8s.newrelic.com resources: @@ -129,4 +149,3 @@ rules: - get - patch - update - diff --git a/configs/role.yaml b/configs/role.yaml index b41e666..85af48f 100644 --- a/configs/role.yaml +++ b/configs/role.yaml @@ -26,6 +26,26 @@ rules: - get - patch - update +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannel + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nr.k8s.newrelic.com + resources: + - alertschannel/status + verbs: + - get + - patch + - update - apiGroups: - nr.k8s.newrelic.com resources: diff --git a/configs/webhook/manifests.yaml b/configs/webhook/manifests.yaml index 0e8994c..b623cb1 100644 --- a/configs/webhook/manifests.yaml +++ b/configs/webhook/manifests.yaml @@ -1,233 +1,283 @@ --- -apiVersion: admissionregistration.k8s.io/v1beta1 -kind: MutatingWebhookConfiguration -metadata: - creationTimestamp: null - name: mutating-webhook-configuration -webhooks: -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /mutate-nr-k8s-newrelic-com-v1-apmalertcondition - failurePolicy: Fail - name: mapmalertcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - apmalertconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /mutate-nr-k8s-newrelic-com-v1-nrqlalertcondition - failurePolicy: Fail - name: mnrqlalertcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - nrqlalertconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /mutate-nr-k8s-newrelic-com-v1-policy - failurePolicy: Fail - name: mpolicy.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - policies -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /mutate-nr-k8s-newrelic-com-v1-alertsapmcondition - failurePolicy: Fail - name: malertsapmcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - alertsapmconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /mutate-nr-k8s-newrelic-com-v1-alertsnrqlcondition - failurePolicy: Fail - name: malertsnrqlcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - alertsnrqlconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /mutate-nr-k8s-newrelic-com-v1-alertspolicy - failurePolicy: Fail - name: malertspolicy.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - alertspolicies - - ---- -apiVersion: admissionregistration.k8s.io/v1beta1 -kind: ValidatingWebhookConfiguration -metadata: - creationTimestamp: null - name: validating-webhook-configuration -webhooks: -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /validate-nr-k8s-newrelic-com-v1-apmalertcondition - failurePolicy: Fail - name: vapmalertcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - apmalertconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /validate-nr-k8s-newrelic-com-v1-nrqlalertcondition - failurePolicy: Fail - name: vnrqlalertcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - nrqlalertconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /validate-nr-k8s-newrelic-com-v1-policy - failurePolicy: Fail - name: vpolicy.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - policies -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /validate-nr-k8s-newrelic-com-v1-alertsapmcondition - failurePolicy: Fail - name: valertsapmcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - alertsapmconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /validate-nr-k8s-newrelic-com-v1-alertsnrqlcondition - failurePolicy: Fail - name: valertsnrqlcondition.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - alertsnrqlconditions -- clientConfig: - caBundle: Cg== - service: - name: webhook-service - namespace: system - path: /validate-nr-k8s-newrelic-com-v1-alertspolicy - failurePolicy: Fail - name: valertspolicy.kb.io - rules: - - apiGroups: - - nr.k8s.newrelic.com - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - alertspolicies + apiVersion: admissionregistration.k8s.io/v1beta1 + kind: MutatingWebhookConfiguration + metadata: + creationTimestamp: null + name: mutating-webhook-configuration + webhooks: + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-alertsapmcondition + failurePolicy: Fail + name: malertsapmcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertsapmconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-alertsnrqlcondition + failurePolicy: Fail + name: malertsnrqlcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertsnrqlconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-alertspolicy + failurePolicy: Fail + name: malertspolicy.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertspolicies + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-alertschannel + failurePolicy: Fail + name: malertschannel.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertschannels + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-apmalertcondition + failurePolicy: Fail + name: mapmalertcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - apmalertconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-nrqlalertcondition + failurePolicy: Fail + name: mnrqlalertcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - nrqlalertconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-nr-k8s-newrelic-com-v1-policy + failurePolicy: Fail + name: mpolicy.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - policies + sideEffects: None + + --- + apiVersion: admissionregistration.k8s.io/v1beta1 + kind: ValidatingWebhookConfiguration + metadata: + creationTimestamp: null + name: validating-webhook-configuration + webhooks: + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-alertsapmcondition + failurePolicy: Fail + name: valertsapmcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertsapmconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-alertsnrqlcondition + failurePolicy: Fail + name: valertsnrqlcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertsnrqlconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-alertspolicy + failurePolicy: Fail + name: valertspolicy.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertspolicies + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-alertschannel + failurePolicy: Fail + name: valertschannel.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - alertschannels + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-apmalertcondition + failurePolicy: Fail + name: vapmalertcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - apmalertconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-nrqlalertcondition + failurePolicy: Fail + name: vnrqlalertcondition.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - nrqlalertconditions + sideEffects: None + - clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-nr-k8s-newrelic-com-v1-policy + failurePolicy: Fail + name: vpolicy.kb.io + rules: + - apiGroups: + - nr.k8s.newrelic.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - policies + sideEffects: None + \ No newline at end of file diff --git a/controllers/alerts_apmcondition_controller.go b/controllers/alerts_apmcondition_controller.go index cb337fb..a210e1b 100644 --- a/controllers/alerts_apmcondition_controller.go +++ b/controllers/alerts_apmcondition_controller.go @@ -189,6 +189,7 @@ func (r *AlertsAPMConditionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, func (r *AlertsAPMConditionReconciler) SetupWithManager(mgr ctrl.Manager) error { r.AlertClientFunc = interfaces.InitializeAlertsClient + return ctrl.NewControllerManagedBy(mgr). For(&nralertsv1.AlertsAPMCondition{}). Complete(r) @@ -197,6 +198,7 @@ func (r *AlertsAPMConditionReconciler) SetupWithManager(mgr ctrl.Manager) error func (r *AlertsAPMConditionReconciler) checkForExistingCondition(condition *nralertsv1.AlertsAPMCondition) { if condition.Status.ConditionID == 0 { r.Log.Info("Checking for existing condition", "conditionName", condition.Name) + //if no conditionId, get list of conditions and compare name existingPolicyIDInt, err := strconv.Atoi(condition.Spec.ExistingPolicyID) if err != nil { @@ -225,6 +227,7 @@ func (r *AlertsAPMConditionReconciler) checkForExistingCondition(condition *nral func (r *AlertsAPMConditionReconciler) deleteNewRelicAlertCondition(condition nralertsv1.AlertsAPMCondition) error { r.Log.Info("Deleting condition", "conditionName", condition.Spec.Name) + _, err := r.Alerts.DeleteCondition(condition.Status.ConditionID) if err != nil { r.Log.Error(err, "Error deleting condition", @@ -232,24 +235,30 @@ func (r *AlertsAPMConditionReconciler) deleteNewRelicAlertCondition(condition nr "region", condition.Spec.Region, "Api Key", interfaces.PartialAPIKey(r.apiKey), ) + return err } + return nil } func (r *AlertsAPMConditionReconciler) getAPIKeyOrSecret(condition nralertsv1.AlertsAPMCondition) string { - if condition.Spec.APIKey != "" { return condition.Spec.APIKey } + if condition.Spec.APIKeySecret != (nralertsv1.NewRelicAPIKeySecret{}) { - key := types.NamespacedName{Namespace: condition.Spec.APIKeySecret.Namespace, Name: condition.Spec.APIKeySecret.Name} var apiKeySecret v1.Secret + + key := types.NamespacedName{Namespace: condition.Spec.APIKeySecret.Namespace, Name: condition.Spec.APIKeySecret.Name} + if getErr := r.Client.Get(context.Background(), key, &apiKeySecret); getErr != nil { r.Log.Error(getErr, "Error retrieving secret", "secret", apiKeySecret) return "" } + return string(apiKeySecret.Data[condition.Spec.APIKeySecret.KeyName]) } + return "" } diff --git a/controllers/alerts_apmcondition_controller_integration_test.go b/controllers/alerts_apmcondition_controller_integration_test.go index 2c54c4a..bedc6e8 100644 --- a/controllers/alerts_apmcondition_controller_integration_test.go +++ b/controllers/alerts_apmcondition_controller_integration_test.go @@ -23,14 +23,6 @@ import ( ) var _ = Describe("ApmCondition reconciliation", func() { - BeforeEach(func() { - err := ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-namespace", - }, - })) - Expect(err).ToNot(HaveOccurred()) - }) var ( ctx context.Context r *AlertsAPMConditionReconciler @@ -40,7 +32,15 @@ var _ = Describe("ApmCondition reconciliation", func() { secret *v1.Secret fakeAlertFunc func(string, string) (interfaces.NewRelicAlertsClient, error) ) + BeforeEach(func() { + err := ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-namespace", + }, + })) + Expect(err).ToNot(HaveOccurred()) + ctx = context.Background() alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} @@ -49,10 +49,12 @@ var _ = Describe("ApmCondition reconciliation", func() { a.ID = 111 return &a, nil } + alertsClient.UpdateConditionStub = func(a alerts.Condition) (*alerts.Condition, error) { a.ID = 112 return &a, nil } + alertsClient.ListConditionsStub = func(int) ([]*alerts.Condition, error) { var a []*alerts.Condition a = append(a, &alerts.Condition{ @@ -118,16 +120,16 @@ var _ = Describe("ApmCondition reconciliation", func() { ConditionID: 0, }, } + namespacedName = types.NamespacedName{ Namespace: "default", Name: "test-condition", } - request = ctrl.Request{NamespacedName: namespacedName} + request = ctrl.Request{NamespacedName: namespacedName} }) Context("When starting with no conditions", func() { - Context("and given a new AlertsAPMCondition", func() { Context("with a valid condition", func() { It("should create that condition via the AlertClient", func() { @@ -193,8 +195,8 @@ var _ = Describe("ApmCondition reconciliation", func() { } Expect(ignoreAlreadyExists(k8sClient.Create(ctx, secret))).To(Succeed()) }) - It("should create that condition via the AlertClient", func() { + It("should create that condition via the AlertClient", func() { err := k8sClient.Create(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -283,7 +285,6 @@ var _ = Describe("ApmCondition reconciliation", func() { }) Context("with a valid condition", func() { - It("does not create a new condition", func() { err := k8sClient.Create(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -322,7 +323,6 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Status.AppliedSpec).To(Equal(&condition.Spec)) }) - }) }) @@ -419,7 +419,6 @@ var _ = Describe("ApmCondition reconciliation", func() { }) Context("When starting with an existing condition", func() { - Context("and deleting a AlertsAPMCondition", func() { BeforeEach(func() { err := k8sClient.Create(ctx, condition) @@ -434,6 +433,7 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).ToNot(HaveOccurred()) }) + Context("with a valid condition", func() { It("should delete that condition via the AlertClient", func() { err := k8sClient.Delete(ctx, condition) @@ -462,14 +462,15 @@ var _ = Describe("ApmCondition reconciliation", func() { }) }) - Context("with a condition with no condition ID", func() { + Context("with a condition with no condition ID", func() { BeforeEach(func() { condition.Status.ConditionID = 0 err := k8sClient.Update(ctx, condition) Expect(err).ToNot(HaveOccurred()) }) + It("should just remove the finalizer and delete", func() { err := k8sClient.Delete(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -487,17 +488,15 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) Context("when the Alerts API reports no condition found ", func() { - BeforeEach(func() { alertsClient.DeleteConditionStub = func(int) (*alerts.Condition, error) { return &alerts.Condition{}, errors.New("resource not found") } - }) + It("should just remove the finalizer and delete", func() { err := k8sClient.Delete(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -515,9 +514,7 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) - }) }) }) diff --git a/controllers/alerts_nrqlcondition_controller.go b/controllers/alerts_nrqlcondition_controller.go index 3da4f36..430d122 100644 --- a/controllers/alerts_nrqlcondition_controller.go +++ b/controllers/alerts_nrqlcondition_controller.go @@ -60,6 +60,7 @@ func (r *AlertsNrqlConditionReconciler) Reconcile(req ctrl.Request) (ctrl.Result r.Log.Info("starting reconcile action") var condition nrv1.AlertsNrqlCondition + err := r.Client.Get(ctx, req.NamespacedName, &condition) if err != nil { if strings.Contains(err.Error(), " not found") { @@ -213,6 +214,7 @@ func (r *AlertsNrqlConditionReconciler) checkForExistingCondition(condition *nrv func (r *AlertsNrqlConditionReconciler) SetupWithManager(mgr ctrl.Manager) error { r.AlertClientFunc = interfaces.InitializeAlertsClient + return ctrl.NewControllerManagedBy(mgr). For(&nrv1.AlertsNrqlCondition{}). Complete(r) diff --git a/controllers/alerts_nrqlcondition_controller_integration_test.go b/controllers/alerts_nrqlcondition_controller_integration_test.go index d1c2131..0aa9911 100644 --- a/controllers/alerts_nrqlcondition_controller_integration_test.go +++ b/controllers/alerts_nrqlcondition_controller_integration_test.go @@ -235,7 +235,6 @@ var _ = Describe("AlertsNrqlCondition reconciliation", func() { ConditionID: "0", }, } - }) Context("with a valid condition", func() { @@ -441,7 +440,6 @@ var _ = Describe("AlertsNrqlCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) }) }) diff --git a/controllers/alerts_policy_controller.go b/controllers/alerts_policy_controller.go index c753301..67e3d64 100644 --- a/controllers/alerts_policy_controller.go +++ b/controllers/alerts_policy_controller.go @@ -133,6 +133,7 @@ func (r *AlertsPolicyReconciler) createAlertsPolicy(policy *nrv1.AlertsPolicy) e "region", policy.Spec.Region, "apiKey", interfaces.PartialAPIKey(r.apiKey), ) + return err } @@ -141,6 +142,7 @@ func (r *AlertsPolicyReconciler) createAlertsPolicy(policy *nrv1.AlertsPolicy) e errConditions := r.createConditions(policy) if errConditions != nil { r.Log.Error(errConditions, "error creating or updating conditions") + return errConditions } r.Log.Info("policy after condition creation", "policyCondition", policy.Spec.Conditions, "pointer", &policy) @@ -258,6 +260,7 @@ func (r *AlertsPolicyReconciler) updateNrqlCondition(policy *nrv1.AlertsPolicy, nrqlCondition.Spec.AccountID = policy.Spec.AccountID err := r.Client.Update(r.ctx, &nrqlCondition) + return err } @@ -290,6 +293,7 @@ func (r *AlertsPolicyReconciler) updateApmCondition(policy *nrv1.AlertsPolicy, c r.Log.Info("updating existing condition", "alertsAPMCondition", apmCondition) err := r.Client.Update(r.ctx, &apmCondition) + return err } @@ -450,6 +454,7 @@ func (r *AlertsPolicyReconciler) getApmConditionFromAlertsPolicyCondition(condit //throw away the error since empty conditions are expected _ = r.Client.Get(r.ctx, condition.GetNamespace(), &apmCondition) r.Log.Info("retrieved condition", "alertsAPMCondition", apmCondition, "namespace", condition.GetNamespace()) + return } @@ -573,6 +578,7 @@ func (r *AlertsPolicyReconciler) checkForExistingAlertsPolicy(policy *nrv1.Alert if existingAlertsPolicy.Name == policy.Spec.Name { r.Log.Info("matched on existing policy, updating PolicyId", "policyId", existingAlertsPolicy.ID) policy.Status.PolicyID = existingAlertsPolicy.ID + break } } @@ -607,8 +613,10 @@ func (r *AlertsPolicyReconciler) getAPIKeyOrSecret(policy nrv1.AlertsPolicy) str getErr := r.Client.Get(context.Background(), key, &apiKeySecret) if getErr != nil { r.Log.Error(getErr, "Failed to retrieve secret", "secret", apiKeySecret) + return "" } + return string(apiKeySecret.Data[policy.Spec.APIKeySecret.KeyName]) } diff --git a/controllers/alerts_policy_controller_integration_test.go b/controllers/alerts_policy_controller_integration_test.go index ac967c7..56fb000 100644 --- a/controllers/alerts_policy_controller_integration_test.go +++ b/controllers/alerts_policy_controller_integration_test.go @@ -68,7 +68,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Name: "test-alertspolicy", } request = ctrl.Request{NamespacedName: namespacedName} - }) Context("When starting with no policies", func() { @@ -155,8 +154,8 @@ var _ = Describe("alertspolicy reconciliation", func() { Name: "test-alertspolicy", } request = ctrl.Request{NamespacedName: namespacedName} - }) + Context("when creating a valid alertspolicy", func() { It("should create that alertspolicy", func() { @@ -184,6 +183,7 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateAlertsPolicy.Status.PolicyID).To(Equal("333")) }) + It("creates the NRQL condition with attributes from the AlertsPolicy", func() { err := k8sClient.Create(ctx, alertspolicy) Expect(err).ToNot(HaveOccurred()) @@ -205,11 +205,9 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Spec.Nrql.Query).To(Equal("SELECT 1 FROM MyEvents")) Expect(string(endStateCondition.Spec.Terms[0].Priority)).To(Equal("critical")) Expect(endStateCondition.Spec.Enabled).To(BeTrue()) - }) It("creates the NRQL condition with inherited attributes from the AlertsPolicy resource", func() { - err := k8sClient.Create(ctx, alertspolicy) Expect(err).ToNot(HaveOccurred()) @@ -231,9 +229,9 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Spec.ExistingPolicyID).To(Equal("333")) Expect(endStateCondition.Spec.Region).To(Equal(alertspolicy.Spec.Region)) Expect(endStateCondition.Spec.APIKey).To(Equal(alertspolicy.Spec.APIKey)) - }) }) + Context("when the New Relic API returns an error", func() { BeforeEach(func() { mockAlertsClient.CreatePolicyMutationStub = func(int, alerts.AlertsPolicyInput) (*alerts.AlertsPolicy, error) { @@ -309,12 +307,10 @@ var _ = Describe("alertspolicy reconciliation", func() { err = k8sClient.Get(ctx, conditionNameType, &endStateCondition) Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Name).To(Equal("APM Condition")) - }) Context("when creating a valid alertspolicy with conditions with k8 resource name set", func() { It("should create the conditions with an auto-generated name ignoring the manual name", func() { - alertspolicy.Spec.Conditions[0].Name = "my custom name" err := k8sClient.Create(ctx, alertspolicy) @@ -340,7 +336,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Name).ToNot(Equal("my custom name")) }) }) - }) AfterEach(func() { @@ -352,12 +347,10 @@ var _ = Describe("alertspolicy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) Context("When starting with an existing alertspolicy with a NRQL condition", func() { BeforeEach(func() { - conditionSpec = &nrv1.AlertsPolicyConditionSpec{ nrv1.AlertsGenericConditionSpec{ Terms: []nrv1.AlertsNrqlConditionTerm{ @@ -422,6 +415,7 @@ var _ = Describe("alertspolicy reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, alertspolicy) Expect(err).ToNot(HaveOccurred()) }) + Context("and deleting that alertspolicy", func() { It("should successfully delete", func() { err := k8sClient.Delete(ctx, alertspolicy) @@ -434,8 +428,8 @@ var _ = Describe("alertspolicy reconciliation", func() { var endStateAlertsPolicy nrv1.AlertsPolicy err = k8sClient.Get(ctx, namespacedName, &endStateAlertsPolicy) Expect(err).NotTo(BeNil()) - }) + It("should delete the condition", func() { err := k8sClient.Delete(ctx, alertspolicy) Expect(err).ToNot(HaveOccurred()) @@ -449,17 +443,19 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Spec.Name).ToNot(Equal(alertspolicy.Spec.Conditions[0].Spec.Name)) - }) }) + Context("and New Relic API returns a 404", func() { BeforeEach(func() { mockAlertsClient.DeletePolicyMutationStub = func(int, string) (*alerts.AlertsPolicy, error) { return &alerts.AlertsPolicy{}, errors.New("Imaginary 404 Failure") } }) + It("should succeed as if a previous reconcile already deleted the alertspolicy", func() { }) + AfterEach(func() { mockAlertsClient.DeletePolicyMutationStub = func(int, string) (*alerts.AlertsPolicy, error) { return &alerts.AlertsPolicy{}, nil @@ -571,7 +567,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Name).To(Equal(initialConditionName)) }) - }) Context("and updating that alertspolicy", func() { @@ -593,7 +588,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(mockAlertsClient.UpdatePolicyMutationCallCount()).To(Equal(1)) }) - }) Context("and updating a condition name", func() { @@ -651,7 +645,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).ToNot(BeNil()) Expect(originalCondition.Spec.Name).To(Equal("")) }) - }) Context("and updating a condition ", func() { @@ -684,7 +677,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Nrql.Query).To(Equal("SELECT count(*) FROM MyEvent")) Expect(endStateCondition.Name).To(Equal(originalConditionName)) - }) It("should set the inherited values on the updated condition", func() { @@ -711,9 +703,7 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Name).To(Equal(originalConditionName)) Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) - }) - }) Context("and adding another condition ", func() { @@ -775,7 +765,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) }) - }) Context("and when the alerts client returns an error", func() { @@ -785,6 +774,7 @@ var _ = Describe("alertspolicy reconciliation", func() { } alertspolicy.Spec.IncidentPreference = "PER_CONDITION_AND_TARGET" }) + It("should return an error", func() { err := k8sClient.Update(ctx, alertspolicy) Expect(err).ToNot(HaveOccurred()) @@ -803,7 +793,6 @@ var _ = Describe("alertspolicy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) Context("When starting with an existing alertspolicy with an APM condition", func() { @@ -872,7 +861,6 @@ var _ = Describe("alertspolicy reconciliation", func() { }) Context("and making no changes ", func() { - It("should not try to update or create new conditions", func() { initialConditionName := alertspolicy.Spec.Conditions[0].Name alertspolicy.Spec.Conditions[0].Name = "" @@ -902,7 +890,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Name).To(Equal(initialConditionName)) Expect(endStateCondition.Spec.Name).To(Equal("APM Condition")) }) - }) Context("and updating that alertspolicy", func() { @@ -922,9 +909,7 @@ var _ = Describe("alertspolicy reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, &endStateAlertsPolicy) Expect(err).To(BeNil()) Expect(mockAlertsClient.UpdatePolicyMutationCallCount()).To(Equal(1)) - }) - }) Context("and updating a condition name", func() { @@ -982,7 +967,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).ToNot(BeNil()) Expect(originalCondition.Spec.Name).To(Equal("")) }) - }) Context("and updating a condition ", func() { @@ -1015,7 +999,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Metric).To(Equal("Custom/bar")) Expect(endStateCondition.Name).To(Equal(originalConditionName)) - }) It("should set the inherited values on the updated condition", func() { @@ -1041,9 +1024,7 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Name).To(Equal(originalConditionName)) Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) - }) - }) Context("and adding another apm condition ", func() { @@ -1103,7 +1084,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) }) - }) Context("and when the alerts client returns an error", func() { @@ -1113,6 +1093,7 @@ var _ = Describe("alertspolicy reconciliation", func() { } alertspolicy.Spec.IncidentPreference = "PER_CONDITION_AND_TARGET" }) + It("should return an error", func() { err := k8sClient.Update(ctx, alertspolicy) Expect(err).ToNot(HaveOccurred()) @@ -1131,7 +1112,6 @@ var _ = Describe("alertspolicy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) Context("When starting with an existing alertspolicy with two NRQL conditions", func() { @@ -1242,9 +1222,7 @@ var _ = Describe("alertspolicy reconciliation", func() { Context("and removing the second condition ", func() { BeforeEach(func() { - alertspolicy.Spec.Conditions = []nrv1.AlertsPolicyCondition{alertspolicy.Spec.Conditions[0]} - }) It("should remove second condition ", func() { @@ -1275,7 +1253,6 @@ var _ = Describe("alertspolicy reconciliation", func() { err = k8sClient.Get(ctx, deletedConditionNamespace, &deletedCondition) Expect(err).ToNot(BeNil()) Expect(deletedCondition.Name).To(Equal("")) - }) It("should not call the alerts API ", func() { @@ -1287,7 +1264,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).ToNot(HaveOccurred()) Expect(mockAlertsClient.UpdatePolicyMutationCallCount()).To(Equal(0)) }) - }) Context("and removing the first condition ", func() { @@ -1316,7 +1292,6 @@ var _ = Describe("alertspolicy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Name).To(Equal("second alert condition")) }) - }) Context("and when the alerts client returns an error", func() { @@ -1326,6 +1301,7 @@ var _ = Describe("alertspolicy reconciliation", func() { } alertspolicy.Spec.IncidentPreference = "PER_CONDITION_AND_TARGET" }) + It("should return an error", func() { err := k8sClient.Update(ctx, alertspolicy) Expect(err).ToNot(HaveOccurred()) @@ -1344,7 +1320,5 @@ var _ = Describe("alertspolicy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) - }) diff --git a/controllers/alertschannel_controller.go b/controllers/alertschannel_controller.go new file mode 100644 index 0000000..5ace8dd --- /dev/null +++ b/controllers/alertschannel_controller.go @@ -0,0 +1,405 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "errors" + "reflect" + "sort" + "strconv" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/newrelic/newrelic-client-go/pkg/alerts" + + nrv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" + "github.com/newrelic/newrelic-kubernetes-operator/interfaces" +) + +// AlertsChannelReconciler reconciles a AlertsChannel object +type AlertsChannelReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + AlertClientFunc func(string, string) (interfaces.NewRelicAlertsClient, error) + apiKey string + Alerts interfaces.NewRelicAlertsClient + ctx context.Context +} + +// +kubebuilder:rbac:groups=nr.k8s.newrelic.com,resources=alertschannel,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=nr.k8s.newrelic.com,resources=alertschannel/status,verbs=get;update;patch + +//Reconcile - Main processing loop for AlertsChannel reconciliation +func (r *AlertsChannelReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + var alertsChannel nrv1.AlertsChannel + + r.ctx = context.Background() + r.Log.WithValues("alertsChannel", req.NamespacedName) + + err := r.Client.Get(r.ctx, req.NamespacedName, &alertsChannel) + if err != nil { + if strings.Contains(err.Error(), " not found") { + r.Log.Info("AlertsChannel 'not found' after being deleted. This is expected and no cause for alarm", "error", err) + return ctrl.Result{}, nil + } + r.Log.Error(err, "Failed to GET alertsChannel", "name", req.NamespacedName.String()) + return ctrl.Result{}, nil + } + + r.Log.Info("alertsChannel", "alertsChannel.Spec", alertsChannel.Spec, "alertsChannel.status.applied", alertsChannel.Status.AppliedSpec) + + r.apiKey = r.getAPIKeyOrSecret(alertsChannel) + if r.apiKey == "" { + return ctrl.Result{}, errors.New("api key is blank") + } + + //initial alertsClient + alertsClient, errAlertsClient := r.AlertClientFunc(r.apiKey, alertsChannel.Spec.Region) + + if errAlertsClient != nil { + r.Log.Error(errAlertsClient, "Failed to create AlertsClient") + return ctrl.Result{}, errAlertsClient + } + r.Alerts = alertsClient + + deleteFinalizer := "alertschannels.finalizers.nr.k8s.newrelic.com" + + //examine DeletionTimestamp to determine if object is under deletion + if alertsChannel.DeletionTimestamp.IsZero() { + if !containsString(alertsChannel.Finalizers, deleteFinalizer) { + alertsChannel.Finalizers = append(alertsChannel.Finalizers, deleteFinalizer) + } + } else { + err := r.deleteAlertsChannel(&alertsChannel, deleteFinalizer) + if err != nil { + r.Log.Error(err, "error deleting channel", "name", alertsChannel.Name) + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + if reflect.DeepEqual(&alertsChannel.Spec, alertsChannel.Status.AppliedSpec) { + return ctrl.Result{}, nil + } + + r.Log.Info("Reconciling", "alertsChannel", alertsChannel.Name) + + r.checkForExistingAlertsChannel(&alertsChannel) + + if alertsChannel.Status.ChannelID != 0 { + err := r.updateAlertsChannel(&alertsChannel) + if err != nil { + r.Log.Error(err, "error updating alertsChannel") + return ctrl.Result{}, err + } + } else { + err := r.createAlertsChannel(&alertsChannel) + if err != nil { + r.Log.Error(err, "Error creating alertsChannel") + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} + +//SetupWithManager - Sets up Controller for AlertsChannel +func (r *AlertsChannelReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nrv1.AlertsChannel{}). + Complete(r) +} + +func (r *AlertsChannelReconciler) getAPIKeyOrSecret(alertschannel nrv1.AlertsChannel) string { + if alertschannel.Spec.APIKey != "" { + return alertschannel.Spec.APIKey + } + + if alertschannel.Spec.APIKeySecret != (nrv1.NewRelicAPIKeySecret{}) { + var apiKeySecret v1.Secret + + key := types.NamespacedName{Namespace: alertschannel.Spec.APIKeySecret.Namespace, Name: alertschannel.Spec.APIKeySecret.Name} + + getErr := r.Client.Get(context.Background(), key, &apiKeySecret) + if getErr != nil { + r.Log.Error(getErr, "Failed to retrieve secret", "secret", apiKeySecret) + return "" + } + + return string(apiKeySecret.Data[alertschannel.Spec.APIKeySecret.KeyName]) + } + + return "" +} + +func (r *AlertsChannelReconciler) deleteAlertsChannel(alertsChannel *nrv1.AlertsChannel, deleteFinalizer string) (err error) { + r.Log.Info("Deleting AlertsChannel", "name", alertsChannel.Name, "ChannelName", alertsChannel.Spec.Name) + + if alertsChannel.Status.ChannelID != 0 { + _, err = r.Alerts.DeleteChannel(alertsChannel.Status.ChannelID) + if err != nil { + r.Log.Error(err, "error deleting AlertsChannel", "name", alertsChannel.Name, "ChannelName", alertsChannel.Spec.Name) + } + + } + + // Now remove finalizer + alertsChannel.Finalizers = removeString(alertsChannel.Finalizers, deleteFinalizer) + + err = r.Client.Update(r.ctx, alertsChannel) + if err != nil { + r.Log.Error(err, "tried updating condition status", "name", alertsChannel.Name, "Namespace", alertsChannel.Namespace) + return err + } + + return nil +} + +func (r *AlertsChannelReconciler) createAlertsChannel(alertsChannel *nrv1.AlertsChannel) error { + r.Log.Info("Creating AlertsChannel", "name", alertsChannel.Name, "ChannelName", alertsChannel.Spec.Name) + APIChannel := alertsChannel.Spec.APIChannel() + + r.Log.Info("API Payload before calling NR API", "APIChannel", APIChannel) + + createdChannel, err := r.Alerts.CreateChannel(APIChannel) + if err != nil { + r.Log.Error(err, "Error creating AlertsChannel"+alertsChannel.Name) + return err + } + + alertsChannel.Status.ChannelID = createdChannel.ID + + // Now create the links to policies + allPolicyIDs, err := r.getAllPolicyIDs(&alertsChannel.Spec) + + if err != nil { + r.Log.Error(err, "Error getting list of policyIds") + return err + } + + for _, policyID := range allPolicyIDs { + policyChannels, errUpdatePolicies := r.Alerts.UpdatePolicyChannels(policyID, []int{createdChannel.ID}) + if errUpdatePolicies != nil { + r.Log.Error(errUpdatePolicies, "error updating policyAlertsChannels", "policyID", policyID, "conditionID", createdChannel.ID, "policyChannels", policyChannels) + } else { + alertsChannel.Status.AppliedPolicyIDs = append(alertsChannel.Status.AppliedPolicyIDs, policyID) + } + } + + alertsChannel.Status.AppliedSpec = &alertsChannel.Spec + errClientUpdate := r.Client.Update(r.ctx, alertsChannel) + + if errClientUpdate != nil { + r.Log.Error(errClientUpdate, "Error updating channel status", "name", alertsChannel.Name, "Namespace", alertsChannel.Namespace) + return errClientUpdate + } + + return nil +} + +func (r *AlertsChannelReconciler) updateAlertsChannel(alertsChannel *nrv1.AlertsChannel) error { + r.Log.Info("Updating AlertsChannel", "name", alertsChannel.Name, "ChannelName", alertsChannel.Spec.Name) + + //Check to see if update is needed + AppliedPolicyIDs, AppliedErr := r.getAllPolicyIDs(alertsChannel.Status.AppliedSpec) + + if AppliedErr != nil { + r.Log.Error(AppliedErr, "Error getting list of AppliedPolicyIds") + return AppliedErr + } + + IncomingPolicyIDs, incomingErr := r.getAllPolicyIDs(&alertsChannel.Spec) + + if incomingErr != nil { + r.Log.Error(incomingErr, "Error getting list of AppliedPolicyIds") + return incomingErr + } + + r.Log.Info("Updating list of policies attached to AlertsChannel", + "policyIDs", IncomingPolicyIDs, + "AppliedPolicyIDs", AppliedPolicyIDs, + ) + + processedPolicyIDs := make(map[int]bool) + + for _, incomingPolicyID := range IncomingPolicyIDs { + processedPolicyIDs[incomingPolicyID] = false + } + + for _, appliedPolicyID := range AppliedPolicyIDs { + if _, ok := processedPolicyIDs[appliedPolicyID]; ok { + processedPolicyIDs[appliedPolicyID] = true + } else { + r.Log.Info("Need to delete link to", "policyId", appliedPolicyID) + PolicyChannels, err := r.Alerts.DeletePolicyChannel(appliedPolicyID, alertsChannel.Status.ChannelID) + if err != nil { + r.Log.Error(err, "error updating policyAlertsChannels", + "policyID", appliedPolicyID, + "conditionID", alertsChannel.Status.ChannelID, + "PolicyChannels", PolicyChannels, + ) + } + } + } + + //Clear out AppliedPolicyIds + alertsChannel.Status.AppliedPolicyIDs = []int{} + + for policyID, processed := range processedPolicyIDs { + r.Log.Info("processing ", "policyID", policyID, ":processed", processed) + alertsChannel.Status.AppliedPolicyIDs = append(alertsChannel.Status.AppliedPolicyIDs, policyID) + + if !processed { + r.Log.Info("need to add ", "policyID", policyID) + + policyChannels, err := r.Alerts.UpdatePolicyChannels(policyID, []int{alertsChannel.Status.ChannelID}) + if err != nil { + r.Log.Error(err, "error updating policyAlertsChannels", + "policyID", policyID, + "conditionID", alertsChannel.Status.ChannelID, + "policyChannels", policyChannels, + ) + r.Log.Info("policyChannels", "", policyChannels) + } + } + } + + // Now update the AppliedSpec and the k8s object + alertsChannel.Status.AppliedSpec = &alertsChannel.Spec + + err := r.Client.Update(r.ctx, alertsChannel) + if err != nil { + r.Log.Error(err, "Tried updating channel status", "name", alertsChannel.Name, "Namespace", alertsChannel.Namespace) + return err + } + + return nil +} + +func (r *AlertsChannelReconciler) checkForExistingAlertsChannel(alertsChannel *nrv1.AlertsChannel) { + r.Log.Info("Checking for existing Channels matching name: " + alertsChannel.Spec.Name) + retrievedChannels, err := r.Alerts.ListChannels() + + if err != nil { + r.Log.Error(err, "error retrieving list of Channels") + return + } + + // need to delete all non-matching spec channels + for _, channel := range retrievedChannels { + if channel.Name == alertsChannel.Spec.Name { + channelID := channel.ID + channel.ID = 0 + APIChannel := alertsChannel.Spec.APIChannel() + + if reflect.DeepEqual(&APIChannel, channel) { + r.Log.Info("Found matching Alerts Channel name from the New Relic API", "ID", channel.ID) + alertsChannel.Status.ChannelID = channelID + + alertsChannel.Status.AppliedSpec = &alertsChannel.Spec + } + + r.Log.Info("Found non matching channel so need to delete and create channel") + + _, err = r.Alerts.DeleteChannel(channelID) + if err != nil { + r.Log.Error(err, "Error deleting non-matching AlertsChannel via New Relic API") + continue + } + } + } +} + +func (r *AlertsChannelReconciler) getAllPolicyIDs(alertsChannelSpec *nrv1.AlertsChannelSpec) (policyIDs []int, err error) { + var retrievedPolicies []alerts.Policy + policyIDMap := make(map[int]bool) + + for _, policyID := range alertsChannelSpec.Links.PolicyIDs { + policyIDMap[policyID] = true + } + + if len(alertsChannelSpec.Links.PolicyNames) > 0 { + for _, policyName := range alertsChannelSpec.Links.PolicyNames { + alertParams := &alerts.ListPoliciesParams{ + Name: policyName, + } + + retrievedPolicies, err = r.Alerts.ListPolicies(alertParams) + if err != nil { + r.Log.Error(err, "Error getting list of policies") + return + } + + for _, APIPolicy := range retrievedPolicies { + if policyName == APIPolicy.Name { + r.Log.Info("Found match of "+policyName, "policyId", APIPolicy.ID) + policyIDMap[APIPolicy.ID] = true + } + } + } + } + + if len(alertsChannelSpec.Links.PolicyKubernetesObjects) > 0 { + r.Log.Info("Getting PolicyIds from PolicyKubernetesObjects", + "PolicyKubernetesObjects", alertsChannelSpec.Links.PolicyKubernetesObjects, + ) + + for _, policyK8s := range alertsChannelSpec.Links.PolicyKubernetesObjects { + key := types.NamespacedName{ + Namespace: policyK8s.Namespace, + Name: policyK8s.Name, + } + + var k8sPolicy nrv1.AlertsPolicy + + err = r.Client.Get(context.Background(), key, &k8sPolicy) + if err != nil { + r.Log.Error(err, "Failed to retrieve policy", "k8sPolicy", key) + return + } + + policyID, errInt := strconv.Atoi(k8sPolicy.Status.PolicyID) + if errInt != nil { + r.Log.Error(errInt, "Failed to parse policyID as an int") + err = errInt + } + + if policyID != 0 { + policyIDMap[policyID] = true + } else { + r.Log.Info("Retrieved policy " + policyK8s.Name + " but ID was blank") + err = errors.New("Retrieved policy " + policyK8s.Name + " but ID was blank") + return + } + } + } + + for policyID := range policyIDMap { + policyIDs = append(policyIDs, policyID) + } + sort.Ints(policyIDs) + + return +} diff --git a/controllers/alertschannel_controller_test.go b/controllers/alertschannel_controller_test.go new file mode 100644 index 0000000..72c97e0 --- /dev/null +++ b/controllers/alertschannel_controller_test.go @@ -0,0 +1,523 @@ +package controllers + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/newrelic/newrelic-client-go/pkg/alerts" + + nrv1 "github.com/newrelic/newrelic-kubernetes-operator/api/v1" + "github.com/newrelic/newrelic-kubernetes-operator/interfaces" + "github.com/newrelic/newrelic-kubernetes-operator/interfaces/interfacesfakes" +) + +var _ = Describe("AlertsChannel reconciliation", func() { + var ( + ctx context.Context + r *AlertsChannelReconciler + alertsChannel *nrv1.AlertsChannel + request ctrl.Request + namespacedName types.NamespacedName + err error + // secret *v1.Secret + fakeAlertFunc func(string, string) (interfaces.NewRelicAlertsClient, error) + testPolicy nrv1.AlertsPolicy + ) + + BeforeEach(func() { + err = ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-namespace", + }, + })) + Expect(err).ToNot(HaveOccurred()) + + ctx = context.Background() + + alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} + + alertsClient.CreateChannelStub = func(a alerts.Channel) (*alerts.Channel, error) { + a.ID = 543 + return &a, nil + } + + alertsClient.ListChannelsStub = func() ([]*alerts.Channel, error) { + return []*alerts.Channel{}, nil + } + + alertsClient.ListPoliciesStub = func(*alerts.ListPoliciesParams) ([]alerts.Policy, error) { + return []alerts.Policy{ + { + ID: 1122, + Name: "my-policy-name", + }, + }, nil + } + + fakeAlertFunc = func(string, string) (interfaces.NewRelicAlertsClient, error) { + return alertsClient, nil + } + + r = &AlertsChannelReconciler{ + Client: k8sClient, + Log: logf.Log, + AlertClientFunc: fakeAlertFunc, + } + + namespacedName = types.NamespacedName{ + Namespace: "default", + Name: "myalertschannel", + } + + alertsChannel = &nrv1.AlertsChannel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myalertschannel", + Namespace: "default", + }, + Spec: nrv1.AlertsChannelSpec{ + Name: "my alert channel", + APIKey: "api-key", + APIKeySecret: nrv1.NewRelicAPIKeySecret{}, + Region: "US", + Type: "email", + Links: nrv1.ChannelLinks{ + PolicyIDs: []int{ + 1, + 2, + }, + PolicyNames: []string{ + "my-policy-name", + }, + PolicyKubernetesObjects: []metav1.ObjectMeta{ + { + Name: "my-policy", + Namespace: "default", + }, + }, + }, + Configuration: nrv1.AlertsChannelConfiguration{ + Recipients: "me@email.com", + }, + }, + + Status: nrv1.AlertsChannelStatus{ + AppliedSpec: &nrv1.AlertsChannelSpec{}, + ChannelID: 0, + AppliedPolicyIDs: []int{}, + }, + } + namespacedName = types.NamespacedName{ + Namespace: "default", + Name: "myalertschannel", + } + request = ctrl.Request{NamespacedName: namespacedName} + + testPolicy = nrv1.AlertsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "default", + }, + Status: nrv1.AlertsPolicyStatus{ + AppliedSpec: &nrv1.AlertsPolicySpec{}, + PolicyID: "665544", + }, + } + err = ignoreAlreadyExists(k8sClient.Create(ctx, &testPolicy)) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("When starting with no alertsChannels", func() { + Context("and given a new alertsChannel", func() { + Context("with a valid alertsChannelSpec", func() { + BeforeEach(func() { + err = k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should create that alertsChannel via the AlertClient", func() { + Expect(alertsClient.CreateChannelCallCount()).To(Equal(1)) + }) + + It("updates the ChannelID on the kubernetes object", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.ChannelID).To(Equal(543)) + }) + + It("updates the AppliedSpec on the kubernetes object for later comparison", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.AppliedSpec).To(Equal(&alertsChannel.Spec)) + }) + + It("adds a policy by policy name to the alertsChannel", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.AppliedPolicyIDs).To(ContainElement(1122)) + }) + + It("adds a policy by k8s object reference to the alertsChannel", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.AppliedPolicyIDs).To(ContainElement(665544)) + }) + }) + + Context("and given the same policy via policyID and policy name", func() { + BeforeEach(func() { + alertsChannel.Spec.Links = nrv1.ChannelLinks{ + PolicyIDs: []int{ + 1122, + }, + PolicyNames: []string{ + "my-policy-name", + }, + } + }) + + It("should only create a single link object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + Expect(alertsClient.UpdatePolicyChannelsCallCount()).To(Equal(1)) + }) + }) + + Context("and given a AlertsChannel with k8s policy reference that has no policyID", func() { + var existingPolicyID string + + BeforeEach(func() { + key := types.NamespacedName{Name: "my-policy", + Namespace: "default"} + err := k8sClient.Get(ctx, key, &testPolicy) + Expect(err).ToNot(HaveOccurred()) + existingPolicyID = testPolicy.Status.PolicyID + testPolicy.Status.PolicyID = "" + err = k8sClient.Update(ctx, &testPolicy) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should fail the reconcile loop", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + _, err = r.Reconcile(request) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Retrieved policy " + testPolicy.Name + " but ID was blank")) + }) + + AfterEach(func() { + key := types.NamespacedName{Name: "my-policy", + Namespace: "default"} + err := k8sClient.Get(ctx, key, &testPolicy) + Expect(err).ToNot(HaveOccurred()) + testPolicy.Status.PolicyID = existingPolicyID + err = k8sClient.Update(ctx, &testPolicy) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + + Context("and given as new alertsChannel that exists in New Relic", func() { + Context("when the existing Channel is the same as the configuration", func() { + BeforeEach(func() { + alertsClient.ListChannelsStub = func() ([]*alerts.Channel, error) { + return []*alerts.Channel{ + { + ID: 112233, + Name: "my alert channel", + Type: "email", + Configuration: alerts.ChannelConfiguration{ + Recipients: "me@email.com", + }, + }, + }, nil + } + }) + + It("Should not create a new AlertsChannel in New Relic", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + Expect(alertsClient.ListChannelsCallCount()).To(Equal(1)) + Expect(alertsClient.CreateChannelCallCount()).To(Equal(0)) + }) + + It("Should update the ChannelId on the kubernetes object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.ChannelID).To(Equal(112233)) + }) + + It("Should update the AppliedSpec on the kubernetes object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.AppliedSpec).To(Equal(&alertsChannel.Spec)) + }) + }) + + Context("when the existing Channel is different from the configuration", func() { + BeforeEach(func() { + alertsClient.ListChannelsStub = func() ([]*alerts.Channel, error) { + return []*alerts.Channel{ + { + ID: 112233, + Name: "my alert channel", + Type: "email", + Configuration: alerts.ChannelConfiguration{ + Recipients: "me@stuff.com", + }, + }, + }, nil + } + alertsClient.CreateChannelStub = func(a alerts.Channel) (*alerts.Channel, error) { + a.ID = 112244 + return &a, nil + } + + }) + + It("Should delete and create a new AlertsChannel in New Relic", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + Expect(alertsClient.ListChannelsCallCount()).To(Equal(1)) + Expect(alertsClient.CreateChannelCallCount()).To(Equal(1)) + Expect(alertsClient.DeleteChannelCallCount()).To(Equal(1)) + }) + + It("Should update the ChannelId on the kubernetes object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.ChannelID).To(Equal(112244)) + }) + + It("Should update the AppliedSpec on the kubernetes object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.AppliedSpec).To(Equal(&alertsChannel.Spec)) + }) + }) + + Context("when multiple existing Channels are returned from the alerts API", func() { + BeforeEach(func() { + alertsClient.ListChannelsStub = func() ([]*alerts.Channel, error) { + return []*alerts.Channel{ + { + ID: 112233, + Name: "my alert channel", + Type: "email", + Configuration: alerts.ChannelConfiguration{ + Recipients: "me@stuff.com", + }, + }, + { + ID: 112245, + Name: "my alert channel", + Type: "email", + Configuration: alerts.ChannelConfiguration{ + Recipients: "me2@stuff.com", + }, + }, + }, nil + } + alertsClient.CreateChannelStub = func(a alerts.Channel) (*alerts.Channel, error) { + a.ID = 112234 + return &a, nil + } + }) + + It("Should delete both and create a new AlertsChannel in New Relic", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + Expect(alertsClient.ListChannelsCallCount()).To(Equal(1)) + Expect(alertsClient.CreateChannelCallCount()).To(Equal(1)) + Expect(alertsClient.DeleteChannelCallCount()).To(Equal(2)) + }) + + It("Should update the ChannelId on the kubernetes object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.ChannelID).To(Equal(112234)) + }) + + It("Should update the AppliedSpec on the kubernetes object", func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + var endStateAlertsChannel nrv1.AlertsChannel + err = k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(BeNil()) + Expect(endStateAlertsChannel.Status.AppliedSpec).To(Equal(&alertsChannel.Spec)) + }) + }) + }) + + AfterEach(func() { + // Delete the alertsChannel + err := k8sClient.Delete(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // Need to call reconcile to delete finalizer + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("When starting with an existing alertsChannel", func() { + BeforeEach(func() { + err := k8sClient.Create(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("and deleting that alertsChannel", func() { + BeforeEach(func() { + err := k8sClient.Delete(ctx, alertsChannel) + + Expect(err).ToNot(HaveOccurred()) + + // call reconcile + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should delete via the NR API", func() { + Expect(alertsClient.DeleteChannelCallCount()).To(Equal(1)) + }) + + It("Should delete the k8s object", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err := k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(" \"myalertschannel\" not found")) + Expect(endStateAlertsChannel.Name).To(Equal("")) + }) + }) + + Context("and updating that alertsChannel", func() { + BeforeEach(func() { + //Get the object again after creation + err := k8sClient.Get(ctx, namespacedName, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("When adding a new policyID to the list of policyIds", func() { + BeforeEach(func() { + alertsChannel.Spec.Links.PolicyIDs = append(alertsChannel.Spec.Links.PolicyIDs, 4) + err := k8sClient.Update(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should call the NR API", func() { + Expect(alertsClient.UpdatePolicyChannelsCallCount()).To(Equal(5)) //4 existing plus 1 new policy + policyID, _ := alertsClient.UpdatePolicyChannelsArgsForCall(4) + Expect(policyID).To(Equal(4)) + }) + + It("Should update the appliedPolicyIDs", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err := k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).ToNot(HaveOccurred()) + Expect(endStateAlertsChannel.Status.AppliedPolicyIDs).To(ContainElement(4)) + }) + }) + + Context("When removing a policyID to the list of policyIds", func() { + BeforeEach(func() { + alertsChannel.Spec.Links.PolicyIDs = []int{1} + err := k8sClient.Update(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should call the NR API", func() { + Expect(alertsClient.DeletePolicyChannelCallCount()).To(Equal(1)) + }) + + It("Should update the appliedPolicyIDs", func() { + var endStateAlertsChannel nrv1.AlertsChannel + err := k8sClient.Get(ctx, namespacedName, &endStateAlertsChannel) + Expect(err).ToNot(HaveOccurred()) + Expect(endStateAlertsChannel.Status.AppliedPolicyIDs).ToNot(ContainElement(2)) + }) + }) + + AfterEach(func() { + err := k8sClient.Delete(ctx, alertsChannel) + Expect(err).ToNot(HaveOccurred()) + _, err = r.Reconcile(request) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) +}) diff --git a/controllers/apmalertcondition_controller.go b/controllers/apmalertcondition_controller.go index a9bd06a..7418836 100644 --- a/controllers/apmalertcondition_controller.go +++ b/controllers/apmalertcondition_controller.go @@ -181,6 +181,7 @@ func (r *ApmAlertConditionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, func (r *ApmAlertConditionReconciler) SetupWithManager(mgr ctrl.Manager) error { r.AlertClientFunc = interfaces.InitializeAlertsClient + return ctrl.NewControllerManagedBy(mgr). For(&nralertsv1.ApmAlertCondition{}). Complete(r) @@ -220,14 +221,15 @@ func (r *ApmAlertConditionReconciler) deleteNewRelicAlertCondition(condition nra ) return err } + return nil } func (r *ApmAlertConditionReconciler) getAPIKeyOrSecret(condition nralertsv1.ApmAlertCondition) string { - if condition.Spec.APIKey != "" { return condition.Spec.APIKey } + if condition.Spec.APIKeySecret != (nralertsv1.NewRelicAPIKeySecret{}) { key := types.NamespacedName{Namespace: condition.Spec.APIKeySecret.Namespace, Name: condition.Spec.APIKeySecret.Name} var apiKeySecret v1.Secret @@ -235,7 +237,9 @@ func (r *ApmAlertConditionReconciler) getAPIKeyOrSecret(condition nralertsv1.Apm r.Log.Error(getErr, "Error retrieving secret", "secret", apiKeySecret) return "" } + return string(apiKeySecret.Data[condition.Spec.APIKeySecret.KeyName]) } + return "" } diff --git a/controllers/apmalertcondition_controller_integration_test.go b/controllers/apmalertcondition_controller_integration_test.go index 204cb48..9039196 100644 --- a/controllers/apmalertcondition_controller_integration_test.go +++ b/controllers/apmalertcondition_controller_integration_test.go @@ -23,14 +23,6 @@ import ( ) var _ = Describe("ApmCondition reconciliation", func() { - BeforeEach(func() { - err := ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-namespace", - }, - })) - Expect(err).ToNot(HaveOccurred()) - }) var ( ctx context.Context r *ApmAlertConditionReconciler @@ -40,7 +32,15 @@ var _ = Describe("ApmCondition reconciliation", func() { secret *v1.Secret fakeAlertFunc func(string, string) (interfaces.NewRelicAlertsClient, error) ) + BeforeEach(func() { + err := ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-namespace", + }, + })) + Expect(err).ToNot(HaveOccurred()) + ctx = context.Background() alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} @@ -49,10 +49,12 @@ var _ = Describe("ApmCondition reconciliation", func() { a.ID = 111 return &a, nil } + alertsClient.UpdateConditionStub = func(a alerts.Condition) (*alerts.Condition, error) { a.ID = 112 return &a, nil } + alertsClient.ListConditionsStub = func(int) ([]*alerts.Condition, error) { var a []*alerts.Condition a = append(a, &alerts.Condition{ @@ -123,11 +125,9 @@ var _ = Describe("ApmCondition reconciliation", func() { Name: "test-condition", } request = ctrl.Request{NamespacedName: namespacedName} - }) Context("When starting with no conditions", func() { - Context("and given a new ApmAlertCondition", func() { Context("with a valid condition", func() { It("should create that condition via the AlertClient", func() { @@ -193,6 +193,7 @@ var _ = Describe("ApmCondition reconciliation", func() { } Expect(ignoreAlreadyExists(k8sClient.Create(ctx, secret))).To(Succeed()) }) + It("should create that condition via the AlertClient", func() { err := k8sClient.Create(ctx, condition) @@ -283,7 +284,6 @@ var _ = Describe("ApmCondition reconciliation", func() { }) Context("with a valid condition", func() { - It("does not create a new condition", func() { err := k8sClient.Create(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -322,7 +322,6 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Status.AppliedSpec).To(Equal(&condition.Spec)) }) - }) }) @@ -419,7 +418,6 @@ var _ = Describe("ApmCondition reconciliation", func() { }) Context("When starting with an existing condition", func() { - Context("and deleting a ApmAlertCondition", func() { BeforeEach(func() { err := k8sClient.Create(ctx, condition) @@ -432,8 +430,8 @@ var _ = Describe("ApmCondition reconciliation", func() { // change the event after creation via reconciliation err = k8sClient.Get(ctx, namespacedName, condition) Expect(err).ToNot(HaveOccurred()) - }) + Context("with a valid condition", func() { It("should delete that condition via the AlertClient", func() { err := k8sClient.Delete(ctx, condition) @@ -460,16 +458,16 @@ var _ = Describe("ApmCondition reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, &endStateCondition) Expect(err).To(HaveOccurred()) }) - }) - Context("with a condition with no condition ID", func() { + Context("with a condition with no condition ID", func() { BeforeEach(func() { condition.Status.ConditionID = 0 err := k8sClient.Update(ctx, condition) Expect(err).ToNot(HaveOccurred()) }) + It("should just remove the finalizer and delete", func() { err := k8sClient.Delete(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -487,17 +485,16 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) Context("when the Alerts API reports no condition found ", func() { - BeforeEach(func() { alertsClient.DeleteConditionStub = func(int) (*alerts.Condition, error) { return &alerts.Condition{}, errors.New("resource not found") } }) + It("should just remove the finalizer and delete", func() { err := k8sClient.Delete(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -515,9 +512,7 @@ var _ = Describe("ApmCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) - }) }) }) diff --git a/controllers/nrqlalertcondition_controller.go b/controllers/nrqlalertcondition_controller.go index 5b84068..ddae09d 100644 --- a/controllers/nrqlalertcondition_controller.go +++ b/controllers/nrqlalertcondition_controller.go @@ -205,6 +205,7 @@ func (r *NrqlAlertConditionReconciler) checkForExistingCondition(condition *nral func (r *NrqlAlertConditionReconciler) SetupWithManager(mgr ctrl.Manager) error { r.AlertClientFunc = interfaces.InitializeAlertsClient + return ctrl.NewControllerManagedBy(mgr). For(&nralertsv1.NrqlAlertCondition{}). Complete(r) @@ -216,6 +217,7 @@ func containsString(slice []string, s string) bool { return true } } + return false } @@ -228,8 +230,10 @@ func (r *NrqlAlertConditionReconciler) deleteNewRelicAlertCondition(condition nr "region", condition.Spec.Region, "Api Key", interfaces.PartialAPIKey(r.apiKey), ) + return err } + return nil } @@ -240,22 +244,26 @@ func removeString(slice []string, s string) (result []string) { } result = append(result, item) } + return } func (r *NrqlAlertConditionReconciler) getAPIKeyOrSecret(condition nralertsv1.NrqlAlertCondition) string { - if condition.Spec.APIKey != "" { return condition.Spec.APIKey } + if condition.Spec.APIKeySecret != (nralertsv1.NewRelicAPIKeySecret{}) { key := types.NamespacedName{Namespace: condition.Spec.APIKeySecret.Namespace, Name: condition.Spec.APIKeySecret.Name} var apiKeySecret v1.Secret if getErr := r.Client.Get(context.Background(), key, &apiKeySecret); getErr != nil { r.Log.Error(getErr, "Error retrieving secret", "secret", apiKeySecret) + return "" } + return string(apiKeySecret.Data[condition.Spec.APIKeySecret.KeyName]) } + return "" } diff --git a/controllers/nrqlalertcondition_controller_integration_test.go b/controllers/nrqlalertcondition_controller_integration_test.go index 4c8a38e..da45466 100644 --- a/controllers/nrqlalertcondition_controller_integration_test.go +++ b/controllers/nrqlalertcondition_controller_integration_test.go @@ -22,15 +22,6 @@ import ( ) var _ = Describe("NrqlCondition reconciliation", func() { - BeforeEach(func() { - err := ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-namespace", - }, - })) - Expect(err).ToNot(HaveOccurred()) - }) - var ( ctx context.Context r *NrqlAlertConditionReconciler @@ -40,7 +31,15 @@ var _ = Describe("NrqlCondition reconciliation", func() { secret *v1.Secret fakeAlertFunc func(string, string) (interfaces.NewRelicAlertsClient, error) ) + BeforeEach(func() { + err := ignoreAlreadyExists(k8sClient.Create(context.Background(), &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-namespace", + }, + })) + Expect(err).ToNot(HaveOccurred()) + ctx = context.Background() alertsClient = &interfacesfakes.FakeNewRelicAlertsClient{} @@ -49,10 +48,12 @@ var _ = Describe("NrqlCondition reconciliation", func() { a.ID = 111 return &a, nil } + alertsClient.UpdateNrqlConditionStub = func(a alerts.NrqlCondition) (*alerts.NrqlCondition, error) { a.ID = 112 return &a, nil } + alertsClient.ListNrqlConditionsStub = func(int) ([]*alerts.NrqlCondition, error) { var a []*alerts.NrqlCondition a = append(a, &alerts.NrqlCondition{ @@ -118,16 +119,16 @@ var _ = Describe("NrqlCondition reconciliation", func() { ConditionID: 0, }, } + namespacedName = types.NamespacedName{ Namespace: "default", Name: "test-condition", } - request = ctrl.Request{NamespacedName: namespacedName} + request = ctrl.Request{NamespacedName: namespacedName} }) Context("When starting with no conditions", func() { - Context("and given a new NrqlAlertCondition", func() { Context("with a valid condition", func() { It("should create that condition via the AlertClient", func() { @@ -193,6 +194,7 @@ var _ = Describe("NrqlCondition reconciliation", func() { } Expect(ignoreAlreadyExists(k8sClient.Create(ctx, secret))).To(Succeed()) }) + It("should create that condition via the AlertClient", func() { err := k8sClient.Create(ctx, condition) @@ -205,9 +207,9 @@ var _ = Describe("NrqlCondition reconciliation", func() { Expect(alertsClient.CreateNrqlConditionCallCount()).To(Equal(1)) Expect(alertsClient.UpdateNrqlConditionCallCount()).To(Equal(0)) }) + AfterEach(func() { //k8sClient.Delete(ctx, secret) - }) It("updates the ConditionID on the kubernetes object", func() { @@ -283,11 +285,9 @@ var _ = Describe("NrqlCondition reconciliation", func() { ConditionID: 0, }, } - }) Context("with a valid condition", func() { - It("does not create a new condition", func() { err := k8sClient.Create(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -326,7 +326,6 @@ var _ = Describe("NrqlCondition reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Status.AppliedSpec).To(Equal(&condition.Spec)) }) - }) }) @@ -422,7 +421,6 @@ var _ = Describe("NrqlCondition reconciliation", func() { }) Context("When starting with an existing condition", func() { - Context("and deleting a NrqlAlertCondition", func() { BeforeEach(func() { err := k8sClient.Create(ctx, condition) @@ -437,6 +435,7 @@ var _ = Describe("NrqlCondition reconciliation", func() { Expect(err).ToNot(HaveOccurred()) }) + Context("with a valid condition", func() { It("should delete that condition via the AlertClient", func() { err := k8sClient.Delete(ctx, condition) @@ -463,16 +462,16 @@ var _ = Describe("NrqlCondition reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, &endStateCondition) Expect(err).To(HaveOccurred()) }) - }) - Context("with a condition with no condition ID", func() { + Context("with a condition with no condition ID", func() { BeforeEach(func() { condition.Status.ConditionID = 0 err := k8sClient.Update(ctx, condition) Expect(err).ToNot(HaveOccurred()) }) + It("should just remove the finalizer and delete", func() { err := k8sClient.Delete(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -490,17 +489,15 @@ var _ = Describe("NrqlCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) Context("when the Alerts API reports no condition found ", func() { - BeforeEach(func() { alertsClient.DeleteNrqlConditionStub = func(int) (*alerts.NrqlCondition, error) { return &alerts.NrqlCondition{}, errors.New("resource not found") } - }) + It("should just remove the finalizer and delete", func() { err := k8sClient.Delete(ctx, condition) Expect(err).ToNot(HaveOccurred()) @@ -518,10 +515,7 @@ var _ = Describe("NrqlCondition reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Name).To(Equal("")) }) - }) - }) }) - }) diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go index a2780ed..078cbdb 100644 --- a/controllers/policy_controller.go +++ b/controllers/policy_controller.go @@ -125,6 +125,7 @@ func (r *PolicyReconciler) createPolicy(policy *nrv1.Policy) error { "region", policy.Spec.Region, "Api Key", interfaces.PartialAPIKey(r.apiKey), ) + return err } policy.Status.PolicyID = createdPolicy.ID @@ -132,6 +133,7 @@ func (r *PolicyReconciler) createPolicy(policy *nrv1.Policy) error { errConditions := r.createConditions(policy) if errConditions != nil { r.Log.Error(errConditions, "error creating or updating conditions") + return errConditions } r.Log.Info("policy after condition creation", "policyCondition", policy.Spec.Conditions, "pointer", &policy) @@ -141,13 +143,14 @@ func (r *PolicyReconciler) createPolicy(policy *nrv1.Policy) error { err = r.Client.Update(r.ctx, policy) if err != nil { r.Log.Error(err, "tried updating policy status", "name", policy.Name) + return err } + return nil } func (r *PolicyReconciler) createConditions(policy *nrv1.Policy) error { - r.Log.Info("initial policy creation so create all policies") collectedErrors := new(customErrors.ErrorCollector) for i, condition := range policy.Spec.Conditions { @@ -166,12 +169,13 @@ func (r *PolicyReconciler) createConditions(policy *nrv1.Policy) error { } else { policy.Spec.Conditions[i] = condition } - } + if len(*collectedErrors) > 0 { r.Log.Info("errors encountered creating conditions", "collectoredErrors", collectedErrors) return collectedErrors } + return nil } @@ -234,6 +238,7 @@ func (r *PolicyReconciler) updateNrqlCondition(policy *nrv1.Policy, condition *n if retrievedPolicyCondition.SpecHash() == condition.SpecHash() { r.Log.Info("existing NrqlCondition matches going to next") + return nil } @@ -246,8 +251,7 @@ func (r *PolicyReconciler) updateNrqlCondition(policy *nrv1.Policy, condition *n nrqlAlertCondition.Spec.APIKey = policy.Spec.APIKey nrqlAlertCondition.Spec.APIKeySecret = policy.Spec.APIKeySecret - err := r.Client.Update(r.ctx, &nrqlAlertCondition) - return err + return r.Client.Update(r.ctx, &nrqlAlertCondition) } func (r *PolicyReconciler) updateApmCondition(policy *nrv1.Policy, condition *nrv1.PolicyCondition) error { @@ -276,8 +280,7 @@ func (r *PolicyReconciler) updateApmCondition(policy *nrv1.Policy, condition *nr r.Log.Info("updating existing condition", "apmAlertCondition", apmAlertCondition) - err := r.Client.Update(r.ctx, &apmAlertCondition) - return err + return r.Client.Update(r.ctx, &apmAlertCondition) } func (r *PolicyReconciler) createOrUpdateConditions(policy *nrv1.Policy) error { @@ -415,8 +418,10 @@ func (r *PolicyReconciler) deleteCondition(condition *nrv1.PolicyCondition) erro err := r.Delete(r.ctx, retrievedCondition) if err != nil { r.Log.Error(err, "error deleting condition resource") + return err } + return nil } @@ -425,6 +430,7 @@ func (r *PolicyReconciler) getNrqlConditionFromPolicyCondition(condition *nrv1.P //throw away the error since empty conditions are expected _ = r.Client.Get(r.ctx, condition.GetNamespace(), &nrqlAlertCondition) r.Log.Info("retrieved condition", "nrqlAlertCondition", nrqlAlertCondition, "namespace", condition.GetNamespace()) + return } @@ -433,6 +439,7 @@ func (r *PolicyReconciler) getApmConditionFromPolicyCondition(condition *nrv1.Po //throw away the error since empty conditions are expected _ = r.Client.Get(r.ctx, condition.GetNamespace(), &apmAlertCondition) r.Log.Info("retrieved condition", "apmAlertCondition", apmAlertCondition, "namespace", condition.GetNamespace()) + return } @@ -457,6 +464,7 @@ func (r *PolicyReconciler) updatePolicy(policy *nrv1.Policy) error { "region", policy.Spec.Region, "Api Key", interfaces.PartialAPIKey(r.apiKey), ) + return err } policy.Status.PolicyID = updatedPolicy.ID @@ -465,6 +473,7 @@ func (r *PolicyReconciler) updatePolicy(policy *nrv1.Policy) error { errConditions := r.createOrUpdateConditions(policy) if errConditions != nil { r.Log.Error(errConditions, "error creating or updating conditions") + return errConditions } r.Log.Info("policySpecx before update", "policy.Spec", policy.Spec) @@ -474,8 +483,10 @@ func (r *PolicyReconciler) updatePolicy(policy *nrv1.Policy) error { err = r.Client.Update(r.ctx, policy) if err != nil { r.Log.Error(err, "failed to update policy status", "name", policy.Name) + return err } + return nil } @@ -516,6 +527,7 @@ func (r *PolicyReconciler) deletePolicy(ctx context.Context, policy *nrv1.Policy policy.Finalizers = removeString(policy.Finalizers, deleteFinalizer) if err := r.Client.Update(ctx, policy); err != nil { r.Log.Error(err, "Failed to update k8s records for this policy after successfully deleting the policy via New Relic Alert API") + return ctrl.Result{}, err } } @@ -568,16 +580,18 @@ func (r *PolicyReconciler) deleteNewRelicAlertPolicy(policy *nrv1.Policy) error "region", policy.Spec.Region, "Api Key", interfaces.PartialAPIKey(r.apiKey), ) + return err } + return nil } func (r *PolicyReconciler) getAPIKeyOrSecret(policy nrv1.Policy) string { - if policy.Spec.APIKey != "" { return policy.Spec.APIKey } + if policy.Spec.APIKeySecret != (nrv1.NewRelicAPIKeySecret{}) { key := types.NamespacedName{Namespace: policy.Spec.APIKeySecret.Namespace, Name: policy.Spec.APIKeySecret.Name} var apiKeySecret v1.Secret @@ -586,7 +600,9 @@ func (r *PolicyReconciler) getAPIKeyOrSecret(policy nrv1.Policy) string { r.Log.Error(getErr, "Failed to retrieve secret", "secret", apiKeySecret) return "" } + return string(apiKeySecret.Data[policy.Spec.APIKeySecret.KeyName]) } + return "" } diff --git a/controllers/policy_controller_integration_api_test.go b/controllers/policy_controller_integration_api_test.go index d3fff11..ed4ea30 100644 --- a/controllers/policy_controller_integration_api_test.go +++ b/controllers/policy_controller_integration_api_test.go @@ -37,6 +37,7 @@ func newIntegrationTestClient(t *testing.T) newrelic.NewRelic { } client, _ := interfaces.NewClient(envAPIKey, envRegion) + return *client } diff --git a/controllers/policy_controller_integration_test.go b/controllers/policy_controller_integration_test.go index d25cb1b..9ce8aa6 100644 --- a/controllers/policy_controller_integration_test.go +++ b/controllers/policy_controller_integration_test.go @@ -65,8 +65,8 @@ var _ = Describe("policy reconciliation", func() { Namespace: "default", Name: "test-policy", } - request = ctrl.Request{NamespacedName: namespacedName} + request = ctrl.Request{NamespacedName: namespacedName} }) Context("When starting with no policies", func() { @@ -151,23 +151,21 @@ var _ = Describe("policy reconciliation", func() { Namespace: "default", Name: "test-policy", } - request = ctrl.Request{NamespacedName: namespacedName} + request = ctrl.Request{NamespacedName: namespacedName} }) + Context("when creating a valid policy", func() { It("should create that policy", func() { - err := k8sClient.Create(ctx, policy) Expect(err).ToNot(HaveOccurred()) // call reconcile _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) - }) It("updates the policyId on the Policy resource", func() { - err := k8sClient.Create(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -179,10 +177,9 @@ var _ = Describe("policy reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, &endStatePolicy) Expect(err).To(BeNil()) Expect(endStatePolicy.Status.PolicyID).To(Equal(333)) - }) - It("creates the NRQL condition with attributes from the Policy", func() { + It("creates the NRQL condition with attributes from the Policy", func() { err := k8sClient.Create(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -203,11 +200,9 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Spec.Nrql.Query).To(Equal("SELECT 1 FROM MyEvents")) Expect(endStateCondition.Spec.Terms[0].Priority).To(Equal("critical")) Expect(endStateCondition.Spec.Enabled).To(BeTrue()) - }) It("creates the NRQL condition with inherited attributes from the Policy resource", func() { - err := k8sClient.Create(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -229,9 +224,9 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Spec.ExistingPolicyID).To(Equal(333)) Expect(endStateCondition.Spec.Region).To(Equal(policy.Spec.Region)) Expect(endStateCondition.Spec.APIKey).To(Equal(policy.Spec.APIKey)) - }) }) + Context("when the New Relic API returns an error", func() { BeforeEach(func() { alertsClient.CreatePolicyStub = func(alerts.Policy) (*alerts.Policy, error) { @@ -240,7 +235,6 @@ var _ = Describe("policy reconciliation", func() { }) It("should not update the PolicyID", func() { - createErr := k8sClient.Create(ctx, policy) Expect(createErr).ToNot(HaveOccurred()) @@ -253,14 +247,12 @@ var _ = Describe("policy reconciliation", func() { getErr := k8sClient.Get(ctx, namespacedName, &endStatePolicy) Expect(getErr).ToNot(HaveOccurred()) Expect(endStatePolicy.Status.PolicyID).To(Equal(0)) - }) }) Context("when creating a valid policy with apm conditions", func() { It("should create the conditions", func() { conditionSpec = &nrv1.ConditionSpec{ - GenericConditionSpec: nrv1.GenericConditionSpec{ Terms: []nrv1.AlertConditionTerm{ { @@ -286,6 +278,7 @@ var _ = Describe("policy reconciliation", func() { ViolationCloseTimer: 60, }, } + policy.Spec.Conditions[0] = nrv1.PolicyCondition{ Spec: *conditionSpec, } @@ -310,12 +303,10 @@ var _ = Describe("policy reconciliation", func() { err = k8sClient.Get(ctx, conditionNameType, &endStateCondition) Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Name).To(Equal("APM Condition")) - }) Context("when creating a valid policy with conditions with k8 resource name set", func() { It("should create the conditions with an auto-generated name ignoring the manual name", func() { - policy.Spec.Conditions[0].Name = "my custom name" err := k8sClient.Create(ctx, policy) @@ -341,7 +332,6 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Name).ToNot(Equal("my custom name")) }) }) - }) AfterEach(func() { @@ -353,12 +343,10 @@ var _ = Describe("policy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) Context("When starting with an existing policy with a NRQL condition", func() { BeforeEach(func() { - conditionSpec = &nrv1.ConditionSpec{ nrv1.GenericConditionSpec{ Terms: []nrv1.AlertConditionTerm{ @@ -423,6 +411,7 @@ var _ = Describe("policy reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, policy) Expect(err).ToNot(HaveOccurred()) }) + Context("and deleting that policy", func() { It("should successfully delete", func() { err := k8sClient.Delete(ctx, policy) @@ -435,8 +424,8 @@ var _ = Describe("policy reconciliation", func() { var endStatePolicy nrv1.Policy err = k8sClient.Get(ctx, namespacedName, &endStatePolicy) Expect(err).NotTo(BeNil()) - }) + It("should delete the condition", func() { err := k8sClient.Delete(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -450,17 +439,19 @@ var _ = Describe("policy reconciliation", func() { Expect(err).To(HaveOccurred()) Expect(endStateCondition.Spec.Name).ToNot(Equal(policy.Spec.Conditions[0].Spec.Name)) - }) }) + Context("and New Relic API returns a 404", func() { BeforeEach(func() { alertsClient.DeletePolicyStub = func(int) (*alerts.Policy, error) { return &alerts.Policy{}, errors.New("Imaginary 404 Failure") } }) + It("should succeed as if a previous reconcile already deleted the policy", func() { }) + AfterEach(func() { alertsClient.DeletePolicyStub = func(int) (*alerts.Policy, error) { return &alerts.Policy{}, nil @@ -544,7 +535,6 @@ var _ = Describe("policy reconciliation", func() { }) Context("and making no changes ", func() { - It("should not try to update or create new conditions", func() { initialConditionName := policy.Spec.Conditions[0].Name policy.Spec.Conditions[0].Name = "" @@ -573,7 +563,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Name).To(Equal(initialConditionName)) }) - }) Context("and updating that policy", func() { @@ -593,9 +582,7 @@ var _ = Describe("policy reconciliation", func() { err = k8sClient.Get(ctx, namespacedName, &endStatePolicy) Expect(err).To(BeNil()) Expect(alertsClient.UpdatePolicyCallCount()).To(Equal(1)) - }) - }) Context("and updating a condition name", func() { @@ -653,7 +640,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).ToNot(BeNil()) Expect(originalCondition.Spec.Name).To(Equal("")) }) - }) Context("and updating a condition ", func() { @@ -686,7 +672,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Nrql.Query).To(Equal("SELECT count(*) FROM MyEvent")) Expect(endStateCondition.Name).To(Equal(originalConditionName)) - }) It("should set the inherited values on the updated condition", func() { @@ -713,9 +698,7 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Name).To(Equal(originalConditionName)) Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) - }) - }) Context("and adding another condition ", func() { @@ -777,7 +760,6 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) }) - }) Context("and when the alerts client returns an error", func() { @@ -787,6 +769,7 @@ var _ = Describe("policy reconciliation", func() { } policy.Spec.IncidentPreference = "PER_CONDITION_AND_TARGET" }) + It("should return an error", func() { err := k8sClient.Update(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -805,7 +788,6 @@ var _ = Describe("policy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) Context("When starting with an existing policy with an APM condition", func() { @@ -875,7 +857,6 @@ var _ = Describe("policy reconciliation", func() { }) Context("and making no changes ", func() { - It("should not try to update or create new conditions", func() { initialConditionName := policy.Spec.Conditions[0].Name policy.Spec.Conditions[0].Name = "" @@ -905,7 +886,6 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Name).To(Equal(initialConditionName)) Expect(endStateCondition.Spec.Name).To(Equal("APM Condition")) }) - }) Context("and updating that policy", func() { @@ -927,7 +907,6 @@ var _ = Describe("policy reconciliation", func() { Expect(alertsClient.UpdatePolicyCallCount()).To(Equal(1)) }) - }) Context("and updating a condition name", func() { @@ -985,7 +964,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).ToNot(BeNil()) Expect(originalCondition.Spec.Name).To(Equal("")) }) - }) Context("and updating a condition ", func() { @@ -1018,7 +996,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Metric).To(Equal("Custom/bar")) Expect(endStateCondition.Name).To(Equal(originalConditionName)) - }) It("should set the inherited values on the updated condition", func() { @@ -1044,15 +1021,12 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Name).To(Equal(originalConditionName)) Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) - }) - }) Context("and adding another apm condition ", func() { BeforeEach(func() { secondConditionSpec := nrv1.ConditionSpec{ - GenericConditionSpec: nrv1.GenericConditionSpec{ Terms: []nrv1.AlertConditionTerm{ { @@ -1078,6 +1052,7 @@ var _ = Describe("policy reconciliation", func() { ViolationCloseTimer: 60, }, } + secondCondition := nrv1.PolicyCondition{ Spec: secondConditionSpec, } @@ -1107,7 +1082,6 @@ var _ = Describe("policy reconciliation", func() { Expect(endStateCondition.Spec.Region).To(Equal("us")) Expect(endStateCondition.Spec.APIKey).To(Equal("112233")) }) - }) Context("and when the alerts client returns an error", func() { @@ -1117,6 +1091,7 @@ var _ = Describe("policy reconciliation", func() { } policy.Spec.IncidentPreference = "PER_CONDITION_AND_TARGET" }) + It("should return an error", func() { err := k8sClient.Update(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -1135,7 +1110,6 @@ var _ = Describe("policy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) Context("When starting with an existing policy with two NRQL conditions", func() { @@ -1246,9 +1220,7 @@ var _ = Describe("policy reconciliation", func() { Context("and removing the second condition ", func() { BeforeEach(func() { - policy.Spec.Conditions = []nrv1.PolicyCondition{policy.Spec.Conditions[0]} - }) It("should remove second condition ", func() { @@ -1279,7 +1251,6 @@ var _ = Describe("policy reconciliation", func() { err = k8sClient.Get(ctx, deletedConditionNamespace, &deletedCondition) Expect(err).ToNot(BeNil()) Expect(deletedCondition.Name).To(Equal("")) - }) It("should not call the alerts API ", func() { @@ -1291,7 +1262,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).ToNot(HaveOccurred()) Expect(alertsClient.UpdatePolicyCallCount()).To(Equal(0)) }) - }) Context("and removing the first condition ", func() { @@ -1320,7 +1290,6 @@ var _ = Describe("policy reconciliation", func() { Expect(err).To(BeNil()) Expect(endStateCondition.Spec.Name).To(Equal("second alert condition")) }) - }) Context("and when the alerts client returns an error", func() { @@ -1330,6 +1299,7 @@ var _ = Describe("policy reconciliation", func() { } policy.Spec.IncidentPreference = "PER_CONDITION_AND_TARGET" }) + It("should return an error", func() { err := k8sClient.Update(ctx, policy) Expect(err).ToNot(HaveOccurred()) @@ -1348,7 +1318,5 @@ var _ = Describe("policy reconciliation", func() { _, err = r.Reconcile(request) Expect(err).ToNot(HaveOccurred()) }) - }) - }) diff --git a/controllers/suite_integration_test.go b/controllers/suite_integration_test.go index 842e178..e6fd6fe 100644 --- a/controllers/suite_integration_test.go +++ b/controllers/suite_integration_test.go @@ -1,5 +1,3 @@ -// +build integration - /* Licensed under the Apache License, Version 2.0 (the "License"); @@ -90,5 +88,6 @@ func ignoreAlreadyExists(err error) error { if apierrors.IsAlreadyExists(err) { return nil } + return err } diff --git a/examples/example_alerts_channel.yaml b/examples/example_alerts_channel.yaml new file mode 100644 index 0000000..3437d8e --- /dev/null +++ b/examples/example_alerts_channel.yaml @@ -0,0 +1,24 @@ +apiVersion: nr.k8s.newrelic.com/v1 +kind: AlertsChannel +metadata: + name: my-channel1 +spec: + api_key: + # api_key_secret: + # name: nr-api-key + # namespace: default + # key_name: api-key + name: "my alert channel" + region: "US" + type: "email" + links: + # Policy links can be by NR PolicyID, NR PolicyName OR K8s AlertPolicy object reference + policy_ids: + - 1 + policy_names: + - "k8s created policy" + policy_kubernetes_objects: + - name: "my-policy" + namespace: "default" + configuration: + recipients: "me@email.com" diff --git a/go.sum b/go.sum index 0ddbaa3..dcebe75 100644 --- a/go.sum +++ b/go.sum @@ -551,6 +551,7 @@ github.com/newrelic/newrelic-client-go v0.28.0 h1:+9eGU+QSA48ebmtJdOOWk8UcrXqE5e github.com/newrelic/newrelic-client-go v0.28.0/go.mod h1:6ulrfoZ6BqLGB4AMq0vutrXLmjo6SOYen4oNwatorEY= github.com/newrelic/newrelic-client-go v0.28.1 h1:kr1M8XGLjb4exzbl1NIa67Q4LAYalr/EWeKKd2wpbw8= github.com/newrelic/newrelic-client-go v0.28.1/go.mod h1:6ulrfoZ6BqLGB4AMq0vutrXLmjo6SOYen4oNwatorEY= +github.com/newrelic/newrelic-client-go v0.29.0 h1:iF5RHhoyB4pqh1IbIs/nlpc79gT71apyZfe4RwjS4Nk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= diff --git a/interfaces/interfacesfakes/fake_new_relic_alerts_client.go b/interfaces/interfacesfakes/fake_new_relic_alerts_client.go index fcdebd1..1dd3711 100644 --- a/interfaces/interfacesfakes/fake_new_relic_alerts_client.go +++ b/interfaces/interfacesfakes/fake_new_relic_alerts_client.go @@ -10,6 +10,19 @@ import ( ) type FakeNewRelicAlertsClient struct { + CreateChannelStub func(alerts.Channel) (*alerts.Channel, error) + createChannelMutex sync.RWMutex + createChannelArgsForCall []struct { + arg1 alerts.Channel + } + createChannelReturns struct { + result1 *alerts.Channel + result2 error + } + createChannelReturnsOnCall map[int]struct { + result1 *alerts.Channel + result2 error + } CreateConditionStub func(int, alerts.Condition) (*alerts.Condition, error) createConditionMutex sync.RWMutex createConditionArgsForCall []struct { @@ -80,6 +93,19 @@ type FakeNewRelicAlertsClient struct { result1 *alerts.AlertsPolicy result2 error } + DeleteChannelStub func(int) (*alerts.Channel, error) + deleteChannelMutex sync.RWMutex + deleteChannelArgsForCall []struct { + arg1 int + } + deleteChannelReturns struct { + result1 *alerts.Channel + result2 error + } + deleteChannelReturnsOnCall map[int]struct { + result1 *alerts.Channel + result2 error + } DeleteConditionStub func(int) (*alerts.Condition, error) deleteConditionMutex sync.RWMutex deleteConditionArgsForCall []struct { @@ -133,6 +159,20 @@ type FakeNewRelicAlertsClient struct { result1 *alerts.Policy result2 error } + DeletePolicyChannelStub func(int, int) (*alerts.Channel, error) + deletePolicyChannelMutex sync.RWMutex + deletePolicyChannelArgsForCall []struct { + arg1 int + arg2 int + } + deletePolicyChannelReturns struct { + result1 *alerts.Channel + result2 error + } + deletePolicyChannelReturnsOnCall map[int]struct { + result1 *alerts.Channel + result2 error + } DeletePolicyMutationStub func(int, string) (*alerts.AlertsPolicy, error) deletePolicyMutationMutex sync.RWMutex deletePolicyMutationArgsForCall []struct { @@ -174,6 +214,18 @@ type FakeNewRelicAlertsClient struct { result1 *alerts.Policy result2 error } + ListChannelsStub func() ([]*alerts.Channel, error) + listChannelsMutex sync.RWMutex + listChannelsArgsForCall []struct { + } + listChannelsReturns struct { + result1 []*alerts.Channel + result2 error + } + listChannelsReturnsOnCall map[int]struct { + result1 []*alerts.Channel + result2 error + } ListConditionsStub func(int) ([]*alerts.Condition, error) listConditionsMutex sync.RWMutex listConditionsArgsForCall []struct { @@ -309,6 +361,20 @@ type FakeNewRelicAlertsClient struct { result1 *alerts.Policy result2 error } + UpdatePolicyChannelsStub func(int, []int) (*alerts.PolicyChannels, error) + updatePolicyChannelsMutex sync.RWMutex + updatePolicyChannelsArgsForCall []struct { + arg1 int + arg2 []int + } + updatePolicyChannelsReturns struct { + result1 *alerts.PolicyChannels + result2 error + } + updatePolicyChannelsReturnsOnCall map[int]struct { + result1 *alerts.PolicyChannels + result2 error + } UpdatePolicyMutationStub func(int, string, alerts.AlertsPolicyUpdateInput) (*alerts.AlertsPolicy, error) updatePolicyMutationMutex sync.RWMutex updatePolicyMutationArgsForCall []struct { @@ -328,6 +394,69 @@ type FakeNewRelicAlertsClient struct { invocationsMutex sync.RWMutex } +func (fake *FakeNewRelicAlertsClient) CreateChannel(arg1 alerts.Channel) (*alerts.Channel, error) { + fake.createChannelMutex.Lock() + ret, specificReturn := fake.createChannelReturnsOnCall[len(fake.createChannelArgsForCall)] + fake.createChannelArgsForCall = append(fake.createChannelArgsForCall, struct { + arg1 alerts.Channel + }{arg1}) + fake.recordInvocation("CreateChannel", []interface{}{arg1}) + fake.createChannelMutex.Unlock() + if fake.CreateChannelStub != nil { + return fake.CreateChannelStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.createChannelReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNewRelicAlertsClient) CreateChannelCallCount() int { + fake.createChannelMutex.RLock() + defer fake.createChannelMutex.RUnlock() + return len(fake.createChannelArgsForCall) +} + +func (fake *FakeNewRelicAlertsClient) CreateChannelCalls(stub func(alerts.Channel) (*alerts.Channel, error)) { + fake.createChannelMutex.Lock() + defer fake.createChannelMutex.Unlock() + fake.CreateChannelStub = stub +} + +func (fake *FakeNewRelicAlertsClient) CreateChannelArgsForCall(i int) alerts.Channel { + fake.createChannelMutex.RLock() + defer fake.createChannelMutex.RUnlock() + argsForCall := fake.createChannelArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNewRelicAlertsClient) CreateChannelReturns(result1 *alerts.Channel, result2 error) { + fake.createChannelMutex.Lock() + defer fake.createChannelMutex.Unlock() + fake.CreateChannelStub = nil + fake.createChannelReturns = struct { + result1 *alerts.Channel + result2 error + }{result1, result2} +} + +func (fake *FakeNewRelicAlertsClient) CreateChannelReturnsOnCall(i int, result1 *alerts.Channel, result2 error) { + fake.createChannelMutex.Lock() + defer fake.createChannelMutex.Unlock() + fake.CreateChannelStub = nil + if fake.createChannelReturnsOnCall == nil { + fake.createChannelReturnsOnCall = make(map[int]struct { + result1 *alerts.Channel + result2 error + }) + } + fake.createChannelReturnsOnCall[i] = struct { + result1 *alerts.Channel + result2 error + }{result1, result2} +} + func (fake *FakeNewRelicAlertsClient) CreateCondition(arg1 int, arg2 alerts.Condition) (*alerts.Condition, error) { fake.createConditionMutex.Lock() ret, specificReturn := fake.createConditionReturnsOnCall[len(fake.createConditionArgsForCall)] @@ -648,6 +777,69 @@ func (fake *FakeNewRelicAlertsClient) CreatePolicyMutationReturnsOnCall(i int, r }{result1, result2} } +func (fake *FakeNewRelicAlertsClient) DeleteChannel(arg1 int) (*alerts.Channel, error) { + fake.deleteChannelMutex.Lock() + ret, specificReturn := fake.deleteChannelReturnsOnCall[len(fake.deleteChannelArgsForCall)] + fake.deleteChannelArgsForCall = append(fake.deleteChannelArgsForCall, struct { + arg1 int + }{arg1}) + fake.recordInvocation("DeleteChannel", []interface{}{arg1}) + fake.deleteChannelMutex.Unlock() + if fake.DeleteChannelStub != nil { + return fake.DeleteChannelStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.deleteChannelReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNewRelicAlertsClient) DeleteChannelCallCount() int { + fake.deleteChannelMutex.RLock() + defer fake.deleteChannelMutex.RUnlock() + return len(fake.deleteChannelArgsForCall) +} + +func (fake *FakeNewRelicAlertsClient) DeleteChannelCalls(stub func(int) (*alerts.Channel, error)) { + fake.deleteChannelMutex.Lock() + defer fake.deleteChannelMutex.Unlock() + fake.DeleteChannelStub = stub +} + +func (fake *FakeNewRelicAlertsClient) DeleteChannelArgsForCall(i int) int { + fake.deleteChannelMutex.RLock() + defer fake.deleteChannelMutex.RUnlock() + argsForCall := fake.deleteChannelArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNewRelicAlertsClient) DeleteChannelReturns(result1 *alerts.Channel, result2 error) { + fake.deleteChannelMutex.Lock() + defer fake.deleteChannelMutex.Unlock() + fake.DeleteChannelStub = nil + fake.deleteChannelReturns = struct { + result1 *alerts.Channel + result2 error + }{result1, result2} +} + +func (fake *FakeNewRelicAlertsClient) DeleteChannelReturnsOnCall(i int, result1 *alerts.Channel, result2 error) { + fake.deleteChannelMutex.Lock() + defer fake.deleteChannelMutex.Unlock() + fake.DeleteChannelStub = nil + if fake.deleteChannelReturnsOnCall == nil { + fake.deleteChannelReturnsOnCall = make(map[int]struct { + result1 *alerts.Channel + result2 error + }) + } + fake.deleteChannelReturnsOnCall[i] = struct { + result1 *alerts.Channel + result2 error + }{result1, result2} +} + func (fake *FakeNewRelicAlertsClient) DeleteCondition(arg1 int) (*alerts.Condition, error) { fake.deleteConditionMutex.Lock() ret, specificReturn := fake.deleteConditionReturnsOnCall[len(fake.deleteConditionArgsForCall)] @@ -901,6 +1093,70 @@ func (fake *FakeNewRelicAlertsClient) DeletePolicyReturnsOnCall(i int, result1 * }{result1, result2} } +func (fake *FakeNewRelicAlertsClient) DeletePolicyChannel(arg1 int, arg2 int) (*alerts.Channel, error) { + fake.deletePolicyChannelMutex.Lock() + ret, specificReturn := fake.deletePolicyChannelReturnsOnCall[len(fake.deletePolicyChannelArgsForCall)] + fake.deletePolicyChannelArgsForCall = append(fake.deletePolicyChannelArgsForCall, struct { + arg1 int + arg2 int + }{arg1, arg2}) + fake.recordInvocation("DeletePolicyChannel", []interface{}{arg1, arg2}) + fake.deletePolicyChannelMutex.Unlock() + if fake.DeletePolicyChannelStub != nil { + return fake.DeletePolicyChannelStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.deletePolicyChannelReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNewRelicAlertsClient) DeletePolicyChannelCallCount() int { + fake.deletePolicyChannelMutex.RLock() + defer fake.deletePolicyChannelMutex.RUnlock() + return len(fake.deletePolicyChannelArgsForCall) +} + +func (fake *FakeNewRelicAlertsClient) DeletePolicyChannelCalls(stub func(int, int) (*alerts.Channel, error)) { + fake.deletePolicyChannelMutex.Lock() + defer fake.deletePolicyChannelMutex.Unlock() + fake.DeletePolicyChannelStub = stub +} + +func (fake *FakeNewRelicAlertsClient) DeletePolicyChannelArgsForCall(i int) (int, int) { + fake.deletePolicyChannelMutex.RLock() + defer fake.deletePolicyChannelMutex.RUnlock() + argsForCall := fake.deletePolicyChannelArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNewRelicAlertsClient) DeletePolicyChannelReturns(result1 *alerts.Channel, result2 error) { + fake.deletePolicyChannelMutex.Lock() + defer fake.deletePolicyChannelMutex.Unlock() + fake.DeletePolicyChannelStub = nil + fake.deletePolicyChannelReturns = struct { + result1 *alerts.Channel + result2 error + }{result1, result2} +} + +func (fake *FakeNewRelicAlertsClient) DeletePolicyChannelReturnsOnCall(i int, result1 *alerts.Channel, result2 error) { + fake.deletePolicyChannelMutex.Lock() + defer fake.deletePolicyChannelMutex.Unlock() + fake.DeletePolicyChannelStub = nil + if fake.deletePolicyChannelReturnsOnCall == nil { + fake.deletePolicyChannelReturnsOnCall = make(map[int]struct { + result1 *alerts.Channel + result2 error + }) + } + fake.deletePolicyChannelReturnsOnCall[i] = struct { + result1 *alerts.Channel + result2 error + }{result1, result2} +} + func (fake *FakeNewRelicAlertsClient) DeletePolicyMutation(arg1 int, arg2 string) (*alerts.AlertsPolicy, error) { fake.deletePolicyMutationMutex.Lock() ret, specificReturn := fake.deletePolicyMutationReturnsOnCall[len(fake.deletePolicyMutationArgsForCall)] @@ -1092,6 +1348,61 @@ func (fake *FakeNewRelicAlertsClient) GetPolicyReturnsOnCall(i int, result1 *ale }{result1, result2} } +func (fake *FakeNewRelicAlertsClient) ListChannels() ([]*alerts.Channel, error) { + fake.listChannelsMutex.Lock() + ret, specificReturn := fake.listChannelsReturnsOnCall[len(fake.listChannelsArgsForCall)] + fake.listChannelsArgsForCall = append(fake.listChannelsArgsForCall, struct { + }{}) + fake.recordInvocation("ListChannels", []interface{}{}) + fake.listChannelsMutex.Unlock() + if fake.ListChannelsStub != nil { + return fake.ListChannelsStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.listChannelsReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNewRelicAlertsClient) ListChannelsCallCount() int { + fake.listChannelsMutex.RLock() + defer fake.listChannelsMutex.RUnlock() + return len(fake.listChannelsArgsForCall) +} + +func (fake *FakeNewRelicAlertsClient) ListChannelsCalls(stub func() ([]*alerts.Channel, error)) { + fake.listChannelsMutex.Lock() + defer fake.listChannelsMutex.Unlock() + fake.ListChannelsStub = stub +} + +func (fake *FakeNewRelicAlertsClient) ListChannelsReturns(result1 []*alerts.Channel, result2 error) { + fake.listChannelsMutex.Lock() + defer fake.listChannelsMutex.Unlock() + fake.ListChannelsStub = nil + fake.listChannelsReturns = struct { + result1 []*alerts.Channel + result2 error + }{result1, result2} +} + +func (fake *FakeNewRelicAlertsClient) ListChannelsReturnsOnCall(i int, result1 []*alerts.Channel, result2 error) { + fake.listChannelsMutex.Lock() + defer fake.listChannelsMutex.Unlock() + fake.ListChannelsStub = nil + if fake.listChannelsReturnsOnCall == nil { + fake.listChannelsReturnsOnCall = make(map[int]struct { + result1 []*alerts.Channel + result2 error + }) + } + fake.listChannelsReturnsOnCall[i] = struct { + result1 []*alerts.Channel + result2 error + }{result1, result2} +} + func (fake *FakeNewRelicAlertsClient) ListConditions(arg1 int) ([]*alerts.Condition, error) { fake.listConditionsMutex.Lock() ret, specificReturn := fake.listConditionsReturnsOnCall[len(fake.listConditionsArgsForCall)] @@ -1727,6 +2038,75 @@ func (fake *FakeNewRelicAlertsClient) UpdatePolicyReturnsOnCall(i int, result1 * }{result1, result2} } +func (fake *FakeNewRelicAlertsClient) UpdatePolicyChannels(arg1 int, arg2 []int) (*alerts.PolicyChannels, error) { + var arg2Copy []int + if arg2 != nil { + arg2Copy = make([]int, len(arg2)) + copy(arg2Copy, arg2) + } + fake.updatePolicyChannelsMutex.Lock() + ret, specificReturn := fake.updatePolicyChannelsReturnsOnCall[len(fake.updatePolicyChannelsArgsForCall)] + fake.updatePolicyChannelsArgsForCall = append(fake.updatePolicyChannelsArgsForCall, struct { + arg1 int + arg2 []int + }{arg1, arg2Copy}) + fake.recordInvocation("UpdatePolicyChannels", []interface{}{arg1, arg2Copy}) + fake.updatePolicyChannelsMutex.Unlock() + if fake.UpdatePolicyChannelsStub != nil { + return fake.UpdatePolicyChannelsStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.updatePolicyChannelsReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNewRelicAlertsClient) UpdatePolicyChannelsCallCount() int { + fake.updatePolicyChannelsMutex.RLock() + defer fake.updatePolicyChannelsMutex.RUnlock() + return len(fake.updatePolicyChannelsArgsForCall) +} + +func (fake *FakeNewRelicAlertsClient) UpdatePolicyChannelsCalls(stub func(int, []int) (*alerts.PolicyChannels, error)) { + fake.updatePolicyChannelsMutex.Lock() + defer fake.updatePolicyChannelsMutex.Unlock() + fake.UpdatePolicyChannelsStub = stub +} + +func (fake *FakeNewRelicAlertsClient) UpdatePolicyChannelsArgsForCall(i int) (int, []int) { + fake.updatePolicyChannelsMutex.RLock() + defer fake.updatePolicyChannelsMutex.RUnlock() + argsForCall := fake.updatePolicyChannelsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNewRelicAlertsClient) UpdatePolicyChannelsReturns(result1 *alerts.PolicyChannels, result2 error) { + fake.updatePolicyChannelsMutex.Lock() + defer fake.updatePolicyChannelsMutex.Unlock() + fake.UpdatePolicyChannelsStub = nil + fake.updatePolicyChannelsReturns = struct { + result1 *alerts.PolicyChannels + result2 error + }{result1, result2} +} + +func (fake *FakeNewRelicAlertsClient) UpdatePolicyChannelsReturnsOnCall(i int, result1 *alerts.PolicyChannels, result2 error) { + fake.updatePolicyChannelsMutex.Lock() + defer fake.updatePolicyChannelsMutex.Unlock() + fake.UpdatePolicyChannelsStub = nil + if fake.updatePolicyChannelsReturnsOnCall == nil { + fake.updatePolicyChannelsReturnsOnCall = make(map[int]struct { + result1 *alerts.PolicyChannels + result2 error + }) + } + fake.updatePolicyChannelsReturnsOnCall[i] = struct { + result1 *alerts.PolicyChannels + result2 error + }{result1, result2} +} + func (fake *FakeNewRelicAlertsClient) UpdatePolicyMutation(arg1 int, arg2 string, arg3 alerts.AlertsPolicyUpdateInput) (*alerts.AlertsPolicy, error) { fake.updatePolicyMutationMutex.Lock() ret, specificReturn := fake.updatePolicyMutationReturnsOnCall[len(fake.updatePolicyMutationArgsForCall)] @@ -1795,6 +2175,8 @@ func (fake *FakeNewRelicAlertsClient) UpdatePolicyMutationReturnsOnCall(i int, r func (fake *FakeNewRelicAlertsClient) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.createChannelMutex.RLock() + defer fake.createChannelMutex.RUnlock() fake.createConditionMutex.RLock() defer fake.createConditionMutex.RUnlock() fake.createNrqlConditionMutex.RLock() @@ -1805,6 +2187,8 @@ func (fake *FakeNewRelicAlertsClient) Invocations() map[string][][]interface{} { defer fake.createPolicyMutex.RUnlock() fake.createPolicyMutationMutex.RLock() defer fake.createPolicyMutationMutex.RUnlock() + fake.deleteChannelMutex.RLock() + defer fake.deleteChannelMutex.RUnlock() fake.deleteConditionMutex.RLock() defer fake.deleteConditionMutex.RUnlock() fake.deleteConditionMutationMutex.RLock() @@ -1813,12 +2197,16 @@ func (fake *FakeNewRelicAlertsClient) Invocations() map[string][][]interface{} { defer fake.deleteNrqlConditionMutex.RUnlock() fake.deletePolicyMutex.RLock() defer fake.deletePolicyMutex.RUnlock() + fake.deletePolicyChannelMutex.RLock() + defer fake.deletePolicyChannelMutex.RUnlock() fake.deletePolicyMutationMutex.RLock() defer fake.deletePolicyMutationMutex.RUnlock() fake.getNrqlConditionQueryMutex.RLock() defer fake.getNrqlConditionQueryMutex.RUnlock() fake.getPolicyMutex.RLock() defer fake.getPolicyMutex.RUnlock() + fake.listChannelsMutex.RLock() + defer fake.listChannelsMutex.RUnlock() fake.listConditionsMutex.RLock() defer fake.listConditionsMutex.RUnlock() fake.listNrqlConditionsMutex.RLock() @@ -1839,6 +2227,8 @@ func (fake *FakeNewRelicAlertsClient) Invocations() map[string][][]interface{} { defer fake.updateNrqlConditionStaticMutationMutex.RUnlock() fake.updatePolicyMutex.RLock() defer fake.updatePolicyMutex.RUnlock() + fake.updatePolicyChannelsMutex.RLock() + defer fake.updatePolicyChannelsMutex.RUnlock() fake.updatePolicyMutationMutex.RLock() defer fake.updatePolicyMutationMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/interfaces/new_relic_alert_client.go b/interfaces/new_relic_alert_client.go index 8f61016..78bf52c 100644 --- a/interfaces/new_relic_alert_client.go +++ b/interfaces/new_relic_alert_client.go @@ -26,6 +26,11 @@ type NewRelicAlertsClient interface { UpdatePolicy(alerts.Policy) (*alerts.Policy, error) DeletePolicy(int) (*alerts.Policy, error) ListPolicies(*alerts.ListPoliciesParams) ([]alerts.Policy, error) + CreateChannel(channel alerts.Channel) (*alerts.Channel, error) + DeleteChannel(id int) (*alerts.Channel, error) + ListChannels() ([]*alerts.Channel, error) + UpdatePolicyChannels(policyID int, channelIDs []int) (*alerts.PolicyChannels, error) + DeletePolicyChannel(policyID int, ChannelID int) (*alerts.Channel, error) // NerdGraph CreatePolicyMutation(accountID int, policy alerts.AlertsPolicyInput) (*alerts.AlertsPolicy, error)