Skip to content

Commit

Permalink
Merge pull request #86 from newrelic/addAlertNotifications
Browse files Browse the repository at this point in the history
feat(alertChannel): add initial support for alertsChannel CRD
  • Loading branch information
RobDay-Reynolds authored Jun 20, 2020
2 parents 3d98ad5 + 3670894 commit adf063e
Show file tree
Hide file tree
Showing 54 changed files with 2,870 additions and 463 deletions.
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ resources:
- group: nr
kind: ApmAlertCondition
version: v1
- group: nr
kind: AlertsChannel
version: v1
- group: nr
kind: AlertsNrqlCondition
version: v1
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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). <br>

**examples/example_alerts_channel.yaml**

```yaml
apiVersion: nr.k8s.newrelic.com/v1
kind: AlertsChannel
metadata:
name: my-channel1
spec:
api_key: <your New Relic personal 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: "[email protected]"
```

> <small>**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. </small>



### 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`
Expand Down
9 changes: 3 additions & 6 deletions api/v1/alerts_apmcondition_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -75,7 +75,6 @@ var _ = Describe("alertsAPMCondition_webhook", func() {
})

Context("With an invalid Type", func() {

BeforeEach(func() {
r.Spec.Type = "burritos"
})
Expand All @@ -88,7 +87,6 @@ var _ = Describe("alertsAPMCondition_webhook", func() {
})

Context("With an invalid Metric", func() {

BeforeEach(func() {
r.Spec.Type = "moar burritos"
})
Expand All @@ -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"
Expand All @@ -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",
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion api/v1/alerts_nrqlcondition_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ var _ = Describe("AlertsNrqlConditionSpec", func() {
var condition AlertsNrqlConditionSpec

BeforeEach(func() {

condition = AlertsNrqlConditionSpec{}
condition.Enabled = true
condition.ExistingPolicyID = "42"
Expand Down
5 changes: 5 additions & 0 deletions api/v1/alerts_policy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func (p *AlertsPolicyCondition) SpecHash() uint32 {
strippedAlertsPolicy.Spec.ExistingPolicyID = ""
conditionTemplateSpecHasher := fnv.New32a()
DeepHashObject(conditionTemplateSpecHasher, strippedAlertsPolicy)

return conditionTemplateSpecHasher.Sum32()
}

Expand Down Expand Up @@ -158,6 +159,7 @@ func (in AlertsPolicySpec) Equals(policyToCompare AlertsPolicySpec) bool {
return false
}
}

return true
}

Expand All @@ -166,6 +168,7 @@ func GetAlertsConditionType(condition AlertsPolicyCondition) string {
if condition.Spec.Type == "NRQL" {
return "AlertsNrqlCondition"
}

return "AlertsAPMCondition"
}

Expand All @@ -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
}
14 changes: 11 additions & 3 deletions api/v1/alerts_policy_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -126,23 +128,25 @@ func (r *AlertsPolicy) ValidateDelete() error {
if err != nil {
return err
}

return nil
}

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")
Expand All @@ -156,18 +160,22 @@ 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")
}

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")
}
6 changes: 3 additions & 3 deletions api/v1/alerts_policy_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ var _ = Describe("AlertsPolicy_webhooks", func() {
err := r.ValidateCreate()
Expect(err).ToNot(HaveOccurred())
})

AfterEach(func() {
k8Client.Delete(context.Background(), secret)
})
Expand Down Expand Up @@ -179,9 +180,8 @@ var _ = Describe("AlertsPolicy_webhooks", func() {
})

Describe("Default", func() {
var (
r AlertsPolicy
)
var r AlertsPolicy

conditionSpec := AlertsPolicyConditionSpec{}
conditionSpec.Terms = []AlertsNrqlConditionTerm{
{
Expand Down
91 changes: 91 additions & 0 deletions api/v1/alertschannel_types.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 49 additions & 0 deletions api/v1/alertschannel_types_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
},
}

})

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("[email protected]"))
})
})
})
Loading

0 comments on commit adf063e

Please sign in to comment.