Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(alertChannel): add initial support for alertsChannel CRD #86

Merged
merged 20 commits into from
Jun 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0d81c25
feat(alertChannel): add initial support for alertChannel CRD
thande Jun 10, 2020
3d9d257
refactor(alertschannels): refactor AlertChannel to AlertsChannel
thande Jun 10, 2020
6c2c80e
chore: add tests for AlertsChannel types
thande Jun 10, 2020
e4a086a
chore: add initial webhook validation tests
thande Jun 10, 2020
aec43a1
chore: add matching on kubernetes resource policy
thande Jun 11, 2020
bb962be
chore: update example alerts_channel
thande Jun 11, 2020
3b5788c
chore: update webhook correct resource name
thande Jun 11, 2020
f68ec75
refactor: refactor alertschannel webhooks
thande Jun 11, 2020
07501b5
chore: add basic create/delete of AlertsChannels
thande Jun 11, 2020
ed1ce79
chore: add some alertsChannel tests
thande Jun 11, 2020
a26419c
chore: added support for attaching policies by name or k8s reference
thande Jun 12, 2020
dde05e9
chore: more tests for AlertsChannel controller
thande Jun 12, 2020
82e8bba
Merge branch 'master' into addAlertNotifications
thande Jun 13, 2020
d51ad04
chore: add tests for alertChannel policy link changes
thande Jun 13, 2020
3a1c314
chore: update README with instructions for adding an AlertsChannel
thande Jun 13, 2020
c8b724d
Merge branch 'master' into addAlertNotifications
thande Jun 19, 2020
a6fec14
refactor: refactor alertsChannels to create/delete via controller
thande Jun 19, 2020
0f3e108
chore: Whitespace consistency cleanup
RobDay-Reynolds Jun 19, 2020
f5de881
bug: only create links for any policy once
RobDay-Reynolds Jun 20, 2020
3670894
chore: Merge new objects from upstream into rbac
RobDay-Reynolds Jun 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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