-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Service Level Objective CRD and Controller (#807)
* feat/Adding-SLO-CRD Adding Finalizer unit tests Final changes Moving CRD types into v1alpha1 and fixing RBAC * [slo] make several updates * tweak validation * fix test * Update apis/datadoghq/v1alpha1/datadogslo_types.go Co-authored-by: Fanny Jiang <[email protected]> * Update apis/datadoghq/v1alpha1/datadogslo_types.go Co-authored-by: Fanny Jiang <[email protected]> * Update apis/datadoghq/v1alpha1/datadogslo_types.go Co-authored-by: Fanny Jiang <[email protected]> * Update apis/datadoghq/v1alpha1/datadogslo_types.go Co-authored-by: Fanny Jiang <[email protected]> * incorporate feedback, fix nil pointer on deletion --------- Co-authored-by: Celene <[email protected]> Co-authored-by: Fanny Jiang <[email protected]>
- Loading branch information
1 parent
cde085e
commit 3f40719
Showing
29 changed files
with
2,525 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2023 Datadog, Inc. | ||
|
||
package v1alpha1 | ||
|
||
import ( | ||
"k8s.io/apimachinery/pkg/api/resource" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
// +k8s:openapi-gen=true | ||
type DatadogSLOSpec struct { | ||
// Name is the name of the service level objective. | ||
Name string `json:"name"` | ||
|
||
// Description is a user-defined description of the service level objective. | ||
// Always included in service level objective responses (but may be null). Optional in create/update requests. | ||
Description *string `json:"description,omitempty"` | ||
|
||
// Groups is a list of (up to 100) monitor groups that narrow the scope of a monitor service level objective. | ||
// Included in service level objective responses if it is not empty. | ||
// Optional in create/update requests for monitor service level objectives, but may only be used when the length of the monitor_ids field is one. | ||
// +listType=set | ||
Groups []string `json:"groups,omitempty"` | ||
|
||
// MonitorIDs is a list of monitor IDs that defines the scope of a monitor service level objective. Required if type is monitor. | ||
// +listType=set | ||
MonitorIDs []int64 `json:"monitorIDs,omitempty"` | ||
|
||
// Tags is a list of tags to associate with your service level objective. | ||
// This can help you categorize and filter service level objectives in the service level objectives page of the UI. | ||
// Note: it's not currently possible to filter by these tags when querying via the API. | ||
// +listType=set | ||
Tags []string `json:"tags,omitempty"` | ||
|
||
// Query is the query for a metric-based SLO. Required if type is metric. | ||
// Note that only the `sum by` aggregator is allowed, which sums all request counts. `Average`, `max`, nor `min` request aggregators are not supported. | ||
Query *DatadogSLOQuery `json:"query,omitempty"` | ||
|
||
// Type is the type of the service level objective. | ||
Type DatadogSLOType `json:"type"` | ||
|
||
// The SLO time window options. | ||
Timeframe DatadogSLOTimeFrame `json:"timeframe"` | ||
|
||
// TargetThreshold is the target threshold such that when the service level indicator is above this threshold over the given timeframe, the objective is being met. | ||
TargetThreshold resource.Quantity `json:"targetThreshold"` | ||
|
||
// WarningThreshold is a optional warning threshold such that when the service level indicator is below this value for the given threshold, but above the target threshold, the objective appears in a "warning" state. This value must be greater than the target threshold. | ||
WarningThreshold *resource.Quantity `json:"warningThreshold,omitempty"` | ||
|
||
// ControllerOptions are the optional parameters in the DatadogSLO controller | ||
ControllerOptions *DatadogSLOControllerOptions `json:"controllerOptions,omitempty"` | ||
} | ||
|
||
// +k8s:openapi-gen=true | ||
type DatadogSLOQuery struct { | ||
// Numerator is a Datadog metric query for good events. | ||
Numerator string `json:"numerator"` | ||
// Denominator is a Datadog metric query for total (valid) events. | ||
Denominator string `json:"denominator"` | ||
} | ||
|
||
type DatadogSLOType string | ||
|
||
const ( | ||
DatadogSLOTypeMetric DatadogSLOType = "metric" | ||
DatadogSLOTypeMonitor DatadogSLOType = "monitor" | ||
) | ||
|
||
func (t DatadogSLOType) IsValid() bool { | ||
switch t { | ||
case DatadogSLOTypeMetric, DatadogSLOTypeMonitor: | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
type DatadogSLOTimeFrame string | ||
|
||
const ( | ||
DatadogSLOTimeFrame7d DatadogSLOTimeFrame = "7d" | ||
DatadogSLOTimeFrame30d DatadogSLOTimeFrame = "30d" | ||
DatadogSLOTimeFrame90d DatadogSLOTimeFrame = "90d" | ||
) | ||
|
||
// DatadogSLOControllerOptions defines options in the DatadogSLO controller. | ||
// +k8s:openapi-gen=true | ||
type DatadogSLOControllerOptions struct { | ||
// DisableRequiredTags disables the automatic addition of required tags to SLOs. | ||
DisableRequiredTags *bool `json:"disableRequiredTags,omitempty"` | ||
} | ||
|
||
// DatadogSLOStatus defines the observed state of a DatadogSLO. | ||
// +k8s:openapi-gen=true | ||
type DatadogSLOStatus struct { | ||
// Conditions represents the latest available observations of the state of a DatadogSLO. | ||
// +listType=map | ||
// +listMapKey=type | ||
Conditions []metav1.Condition `json:"conditions,omitempty"` | ||
|
||
// ID is the SLO ID generated in Datadog. | ||
ID string `json:"id,omitempty"` | ||
|
||
// Creator is the identity of the SLO creator. | ||
Creator string `json:"creator,omitempty"` | ||
|
||
// Created is the time the SLO was created. | ||
Created *metav1.Time `json:"created,omitempty"` | ||
|
||
// SyncStatus shows the health of syncing the SLO state to Datadog. | ||
SyncStatus DatadogSLOSyncStatus `json:"syncStatus,omitempty"` | ||
|
||
// LastForceSyncTime is the last time the API SLO was last force synced with the DatadogSLO resource. | ||
LastForceSyncTime *metav1.Time `json:"lastForceSyncTime,omitempty"` | ||
|
||
// CurrentHash tracks the hash of the current DatadogSLOSpec to know | ||
// if the Spec has changed and needs an update. | ||
CurrentHash string `json:"currentHash,omitempty"` | ||
} | ||
|
||
// DatadogSLOSyncStatus is the message reflecting the health of SLO state syncs to Datadog. | ||
type DatadogSLOSyncStatus string | ||
|
||
const ( | ||
// DatadogSLOSyncStatusOK means syncing is OK. | ||
DatadogSLOSyncStatusOK DatadogSLOSyncStatus = "OK" | ||
// DatadogSLOSyncStatusValidateError means there is a SLO validation error. | ||
DatadogSLOSyncStatusValidateError DatadogSLOSyncStatus = "error validating SLO" | ||
// DatadogSLOSyncStatusUpdateError means there is a SLO update error. | ||
DatadogSLOSyncStatusUpdateError DatadogSLOSyncStatus = "error updating SLO" | ||
// DatadogSLOSyncStatusCreateError means there is an error getting the SLO. | ||
DatadogSLOSyncStatusCreateError DatadogSLOSyncStatus = "error creating SLO" | ||
) | ||
|
||
// DatadogSLO allows a user to define and manage datadog SLOs from Kubernetes cluster. | ||
// +kubebuilder:object:root=true | ||
// +kubebuilder:subresource:status | ||
// +kubebuilder:resource:path=datadogslos,scope=Namespaced,shortName=ddslo | ||
// +kubebuilder:printcolumn:name="id",type="string",JSONPath=".status.id" | ||
// +kubebuilder:printcolumn:name="sync status",type="string",JSONPath=".status.syncStatus" | ||
// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" | ||
// +k8s:openapi-gen=true | ||
// +genclient | ||
type DatadogSLO struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ObjectMeta `json:"metadata,omitempty"` | ||
|
||
Spec DatadogSLOSpec `json:"spec,omitempty"` | ||
Status DatadogSLOStatus `json:"status,omitempty"` | ||
} | ||
|
||
// DatadogSLOList contains a list of DatadogSLOs. | ||
// +kubebuilder:object:root=true | ||
type DatadogSLOList struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ListMeta `json:"metadata,omitempty"` | ||
Items []DatadogSLO `json:"items"` | ||
} | ||
|
||
func init() { | ||
SchemeBuilder.Register(&DatadogSLO{}, &DatadogSLOList{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2023 Datadog, Inc. | ||
|
||
package v1alpha1 | ||
|
||
import ( | ||
"fmt" | ||
|
||
utilserrors "k8s.io/apimachinery/pkg/util/errors" | ||
) | ||
|
||
// IsValidDatadogSLO use to check if a DatadogSLOSpec is valid by checking | ||
// that the required fields are defined | ||
func IsValidDatadogSLO(spec *DatadogSLOSpec) error { | ||
var errs []error | ||
if spec.Name == "" { | ||
errs = append(errs, fmt.Errorf("spec.Name must be defined")) | ||
} | ||
|
||
if spec.Type == "" { | ||
errs = append(errs, fmt.Errorf("spec.Type must be defined")) | ||
} | ||
|
||
if spec.Type != "" && !spec.Type.IsValid() { | ||
errs = append(errs, fmt.Errorf("spec.Type must be one of the values: %s or %s", DatadogSLOTypeMonitor, DatadogSLOTypeMetric)) | ||
} | ||
|
||
if spec.Type == DatadogSLOTypeMetric && spec.Query == nil { | ||
errs = append(errs, fmt.Errorf("spec.Query must be defined when spec.Type is metric")) | ||
} | ||
|
||
if spec.Type == DatadogSLOTypeMonitor && len(spec.MonitorIDs) == 0 { | ||
errs = append(errs, fmt.Errorf("spec.MonitorIDs must be defined when spec.Type is monitor")) | ||
} | ||
|
||
if spec.TargetThreshold.AsApproximateFloat64() <= 0 || spec.TargetThreshold.AsApproximateFloat64() >= 100 { | ||
errs = append(errs, fmt.Errorf("spec.TargetThreshold must be greater than 0 and less than 100")) | ||
} | ||
|
||
if spec.WarningThreshold != nil && (spec.WarningThreshold.AsApproximateFloat64() <= 0 || spec.WarningThreshold.AsApproximateFloat64() >= 100) { | ||
errs = append(errs, fmt.Errorf("spec.WarningThreshold must be greater than 0 and less than 100")) | ||
} | ||
|
||
switch spec.Timeframe { | ||
case DatadogSLOTimeFrame7d, DatadogSLOTimeFrame30d, DatadogSLOTimeFrame90d: | ||
break | ||
default: | ||
errs = append(errs, fmt.Errorf("spec.Timeframe must be defined as one of the values: 7d, 30d, or 90d")) | ||
} | ||
|
||
return utilserrors.NewAggregate(errs) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2023 Datadog, Inc. | ||
|
||
package v1alpha1 | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/assert" | ||
"k8s.io/apimachinery/pkg/api/resource" | ||
utilserrors "k8s.io/apimachinery/pkg/util/errors" | ||
) | ||
|
||
func TestIsValidDatadogSLO(t *testing.T) { | ||
|
||
tests := []struct { | ||
name string | ||
spec *DatadogSLOSpec | ||
expected error | ||
}{ | ||
{ | ||
name: "Valid spec", | ||
spec: &DatadogSLOSpec{ | ||
Name: "MySLO", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: DatadogSLOTypeMetric, | ||
TargetThreshold: resource.MustParse("99.99"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "Missing Name", | ||
spec: &DatadogSLOSpec{ | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: DatadogSLOTypeMetric, | ||
TargetThreshold: resource.MustParse("99.99"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
}, | ||
expected: errors.New("spec.Name must be defined"), | ||
}, | ||
{ | ||
name: "Missing Query", | ||
spec: &DatadogSLOSpec{ | ||
Name: "SLO without Query", | ||
Type: DatadogSLOTypeMetric, | ||
TargetThreshold: resource.MustParse("99.99"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
}, | ||
expected: errors.New("spec.Query must be defined when spec.Type is metric"), | ||
}, | ||
{ | ||
name: "Missing Type", | ||
spec: &DatadogSLOSpec{ | ||
Name: "SLO without Type", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
TargetThreshold: resource.MustParse("99.99"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
}, | ||
expected: errors.New("spec.Type must be defined"), | ||
}, | ||
{ | ||
name: "Invalid Type", | ||
spec: &DatadogSLOSpec{ | ||
Name: "MySLO", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: "invalid", | ||
TargetThreshold: resource.MustParse("99.99"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
}, | ||
expected: errors.New("spec.Type must be one of the values: monitor or metric"), | ||
}, | ||
{ | ||
name: "Missing Threshold and Timeframe", | ||
spec: &DatadogSLOSpec{ | ||
Name: "SLO without Thresholds and Timeframe", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: DatadogSLOTypeMetric, | ||
}, | ||
expected: utilserrors.NewAggregate( | ||
[]error{ | ||
errors.New("spec.TargetThreshold must be greater than 0 and less than 100"), | ||
errors.New("spec.Timeframe must be defined as one of the values: 7d, 30d, or 90d"), | ||
}, | ||
), | ||
}, | ||
{ | ||
name: "Missing MonitorIDs", | ||
spec: &DatadogSLOSpec{ | ||
Name: "MySLO", | ||
Query: &DatadogSLOQuery{}, | ||
Type: DatadogSLOTypeMonitor, | ||
TargetThreshold: resource.MustParse("99.99"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
MonitorIDs: []int64{}, | ||
}, | ||
expected: errors.New("spec.MonitorIDs must be defined when spec.Type is monitor"), | ||
}, | ||
{ | ||
name: "Invalid Thresholds", | ||
spec: &DatadogSLOSpec{ | ||
Name: "MySLO", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: DatadogSLOTypeMetric, | ||
TargetThreshold: resource.MustParse("0"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
}, | ||
expected: errors.New("spec.TargetThreshold must be greater than 0 and less than 100"), | ||
}, | ||
{ | ||
name: "Invalid Thresholds Warning", | ||
spec: &DatadogSLOSpec{ | ||
Name: "MySLO", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: DatadogSLOTypeMetric, | ||
TargetThreshold: resource.MustParse("98.00"), | ||
Timeframe: DatadogSLOTimeFrame30d, | ||
WarningThreshold: ptrResourceQuantity(resource.MustParse("0")), | ||
}, | ||
expected: errors.New("spec.WarningThreshold must be greater than 0 and less than 100"), | ||
}, | ||
{ | ||
name: "Invalid Thresholds Timeframe", | ||
spec: &DatadogSLOSpec{ | ||
Name: "MySLO", | ||
Query: &DatadogSLOQuery{ | ||
Numerator: "good", | ||
Denominator: "total", | ||
}, | ||
Type: DatadogSLOTypeMetric, | ||
TargetThreshold: resource.MustParse("98.00"), | ||
Timeframe: "invalid", | ||
}, | ||
expected: errors.New("spec.Timeframe must be defined as one of the values: 7d, 30d, or 90d"), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
result := IsValidDatadogSLO(tt.spec) | ||
if tt.expected != nil { | ||
assert.EqualError(t, result, tt.expected.Error()) | ||
} else { | ||
assert.Nil(t, result) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func ptrResourceQuantity(n resource.Quantity) *resource.Quantity { | ||
return &n | ||
} |
Oops, something went wrong.