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

Allow annotations to control naming behavior #41

Merged
merged 12 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ Here is the list of supported annotations:
* `k8s.cronitor.io/env` - Override the environment for this CronJob.
* `k8s.cronitor.io/tags` - Comma-separated list of tags for this cron job for use within the Cronitor application.
* `k8s.cronitor.io/cronitor-id` - Manually specify an ID for your monitor in Cronitor rather than autogenerating a new one. Use this when you already have jobs you are tracking in Cronitor that you want to keep the history of and you are migrating to the Cronitor agent, or if you are deleting and recreating your `CronJob` objects (e.g., you are migrating clusters or namespaces). You may also use this if you have a single CronJob that you run in different environments (staging, prod, etc.) and you want them all to report to the same monitor in Cronitor in different Cronitor environments.
* `k8s.cronitor.io/id-inference` - Specify how the Cronitor ID is determined. `k8s` uses the Kubernetes UID. `name` hashes the name of the job itself (which is useful when you want standardization across environments)
* `k8s.cronitor.io/cronitor-name` - Manually specify the name within the Cronitor dashboard for this CronJob. Please note if you are using `k8s.cronitor.io/cronitor-id` you must ensure that CronJobs with the same ID also have the same name, or the different names will overwrite each other.
* `k8s.cronitor.io/name-prefix` - Provides control over the prefix of the name. `none` uses the name as-is. `namespace` prepends the Kubernetes namespace. Any other string provided will be prepended to the name as-is.
* `k8s.cronitor.io/cronitor-notify` - Comma-separated list of Notification List `key`s to assign alert destinations.
* `k8s.cronitor.io/cronitor-group` - Group `key` attribute for grouping the monitor within the Cronitor application.
* `k8s.cronitor.io/cronitor-grace-seconds` - The number of seconds that Cronitor should wait after the scheduled execution time before sending an alert. If the monitor is healthy at the end of the period no alert will be sent.
Expand Down Expand Up @@ -80,5 +82,3 @@ Yes, you definitely can! To <strong>exclude</strong> all of your Kubernetes <cod
</details>

[1]: charts/cronitor-kubernetes/values.yaml


4 changes: 2 additions & 2 deletions charts/cronitor-kubernetes/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v1
name: cronitor-kubernetes
description: |
Helm chart to deploy the Cronitor Kubernetes agent to automatically monitor your CronJobs
version: "0.4.8"
appVersion: "0.4.7"
version: "0.5.0"
appVersion: "0.5.0"
15 changes: 15 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
go: &go
tty: true
stdin_open: true
build:
context: .
target: build
entrypoint: go
command: help
volumes:
- .:/usr/src/app:delegated
- gomod:/go/pkg/mod:cached

volumes:
gomod: {}
4 changes: 1 addition & 3 deletions e2e-test/api/tests/test_monitor_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_no_monitors_with_uid_names():
with assume:
# We want the monitor names NOT to be UUIDs. If they are a UUID,
# that means we encountered a bug with the monitor creation somehow
# and defaultName was not set.
# and Name was not set.
with pytest.raises(ValueError):
uuid.UUID(monitor['name'])

Expand Down Expand Up @@ -134,5 +134,3 @@ def test_same_id_should_result_one_monitor():
assert len(pings[monitor_key]) > 0
pings = cronitor_wrapper.get_ping_history_by_monitor(monitor_key=monitor_key, env='env2')
assert len(pings[monitor_key]) > 0


60 changes: 56 additions & 4 deletions pkg/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const (
// via manual instrumentation, and you'd like to use the same Monitor object.
AnnotationCronitorID CronitorAnnotation = "k8s.cronitor.io/cronitor-id"

// AnnotationCronitorName lets you override the defaultName created by the agent to
// AnnotationCronitorName lets you override the Name created by the agent to
// create the monitor in Cronitor with a custom specified name. This is especially useful
// if you are attaching the same CronJob across multiple namespaces/clusters to a single
// Cronitor Monitor across multiple environments.
Expand All @@ -64,6 +64,14 @@ const (

// AnnotationCronitorGraceSeconds lets you provide the number of seconds to wait before sending a failure alert.
AnnotationCronitorGraceSeconds CronitorAnnotation = "k8s.cronitor.io/cronitor-grace-seconds"

// AnnotationIDInference lets you decide how the Cronitor ID is determined.
// The only valid values are "k8s" and "name". Default is "k8s".
AnnotationIDInference CronitorAnnotation = "k8s.cronitor.io/id-inference"

// AnnotationNamePrefix lets you control the prefix of the name.
// Valid options include "none", "namespace" (to prepend namespace/), or any other string, which will be prepended as-is.
AnnotationNamePrefix CronitorAnnotation = "k8s.cronitor.io/name-prefix"
)

type CronitorConfigParser struct {
Expand Down Expand Up @@ -120,13 +128,29 @@ func (cronitorParser CronitorConfigParser) GetSpecifiedCronitorID() string {
return ""
}

// GetCronitorID returns the correct Cronitor monitor ID for the CronJob, defaulting to the CronJob's
// Kubernetes UID if no pre-specified monitor ID is provided by the user.
// GetCronitorID returns the correct Cronitor monitor ID for the CronJob.
// Defaults to the CronJob's Kubernetes UID if no pre-specified monitor ID is provided by the user.
func (cronitorParser CronitorConfigParser) GetCronitorID() string {
// Default behavior
inference := "k8s"

// Check if a specific Cronitor ID is assigned and return it if present
if specifiedId := cronitorParser.GetSpecifiedCronitorID(); specifiedId != "" {
return specifiedId
}
return string(cronitorParser.cronjob.GetUID())

// Override default inference if an annotation is provided
if annotation, ok := cronitorParser.cronjob.Annotations[string(AnnotationIDInference)]; ok && annotation != "" {
inference = annotation
}

// Return the appropriate ID based on the inference
switch inference {
case "name":
return generateHashFromName(cronitorParser.GetCronitorName())
default:
return string(cronitorParser.cronjob.GetUID())
}
}

// GetSpecifiedCronitorName returns the pre-specified Cronitor monitor name, if provided as an annotation
Expand All @@ -139,6 +163,34 @@ func (cronitorParser CronitorConfigParser) GetSpecifiedCronitorName() string {
return ""
}

// GetCronitorName returns the name to be used by Cronitor monitor.
// Allows the namespace to be prepended or not, and allows arbitrary strings as a prefix.
// Defaults to prepending the namespace if no pre-specified monitor name is provided by the user.
func (cronitorParser CronitorConfigParser) GetCronitorName() string {
// Default behavior
prefix := "namespace"
dudo marked this conversation as resolved.
Show resolved Hide resolved

// Check if a specific Cronitor Name is assigned and return it if present
if specifiedName := cronitorParser.GetSpecifiedCronitorName(); specifiedName != "" {
return specifiedName
}

// Check if a prefix annotation exists and override the default if present
if annotation, ok := cronitorParser.cronjob.Annotations[string(AnnotationNamePrefix)]; ok && annotation != "" {
prefix = annotation
}

// Construct the name based on the prefix
switch prefix {
case "namespace":
return fmt.Sprintf("%s/%s", cronitorParser.cronjob.Namespace, cronitorParser.cronjob.Name)
case "none":
return cronitorParser.cronjob.Name
default:
return fmt.Sprintf("%s%s", prefix, cronitorParser.cronjob.Name)
}
}

// Inclusion/exclusion behavior

func (cronitorParser CronitorConfigParser) getDefaultBehavior() defaultBehaviorValue {
Expand Down
97 changes: 96 additions & 1 deletion pkg/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package pkg

import (
"encoding/json"
v1 "k8s.io/api/batch/v1"
"os"
"strconv"
"testing"

v1 "k8s.io/api/batch/v1"
)

func TestCronJobInclusion(t *testing.T) {
Expand Down Expand Up @@ -44,3 +45,97 @@ func TestGetSchedule(t *testing.T) {
t.Errorf("expected schedule \"*/1 * * * *\", got %s", result)
}
}

func TestGetCronitorID(t *testing.T) {
tests := []struct {
name string
annotationIDInference string
annotationCronitorID string
expectedID string
}{
{
name: "hashed name as ID",
annotationIDInference: "name",
annotationCronitorID: "",
expectedID: "3278d16696a89a92d297b7c46bfd286b20dc3896",
},
{
name: "specific cronitor id",
annotationIDInference: "",
annotationCronitorID: "1234",
expectedID: "1234",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
annotations := []Annotation{
{Key: "k8s.cronitor.io/id-inference", Value: tc.annotationIDInference},
{Key: "k8s.cronitor.io/cronitor-id", Value: tc.annotationCronitorID},
}

cronJob, err := CronJobFromAnnotations(annotations)
if err != nil {
t.Fatalf("unexpected error unmarshalling json: %v", err)
}

parser := NewCronitorConfigParser(&cronJob)
if id := parser.GetCronitorID(); id != tc.expectedID {
t.Errorf("expected ID %s, got %s", tc.expectedID, id)
}
})
}
}

func TestGetCronitorName(t *testing.T) {
tests := []struct {
name string
annotationNamePrefix string
annotationCronitorName string
expectedName string
}{
{
name: "default behavior",
annotationNamePrefix: "",
annotationCronitorName: "",
expectedName: "default/test-cronjob",
},
{
name: "no prefix for name",
annotationNamePrefix: "none",
annotationCronitorName: "",
expectedName: "test-cronjob",
},
{
name: "explicit prefix of namespace",
annotationNamePrefix: "namespace",
annotationCronitorName: "",
expectedName: "default/test-cronjob",
},
{
name: "specific cronitor name",
annotationNamePrefix: "",
annotationCronitorName: "foo",
expectedName: "foo",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
annotations := []Annotation{
{Key: "k8s.cronitor.io/name-prefix", Value: tc.annotationNamePrefix},
{Key: "k8s.cronitor.io/cronitor-name", Value: tc.annotationCronitorName},
}

cronJob, err := CronJobFromAnnotations(annotations)
if err != nil {
t.Fatalf("unexpected error unmarshalling json: %v", err)
}

parser := NewCronitorConfigParser(&cronJob)
if name := parser.GetCronitorName(); name != tc.expectedName {
t.Errorf("expected Name %s, got %s", tc.expectedName, name)
}
})
}
}
16 changes: 2 additions & 14 deletions pkg/api/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
// https://docs.google.com/document/d/1erh-fvTkF14jyJGv3DYuN2UalWe6AN49XOUsWHJccso/edit#heading=h.ylm2gai335jy
type CronitorJob struct {
Key string `json:"key"`
DefaultName string `json:"defaultName"`
Name string `json:"name,omitempty"`
DefaultNote string `json:"defaultNote,omitempty"`
Metadata string `json:"metadata"` // This is actually a string rather than a map
Expand All @@ -45,19 +44,14 @@ func (cronitorJob CronitorJob) GetEnvironment() string {
return ""
}

func truncateDefaultName(name string) string {
func truncateName(name string) string {
if len(name) > 100 {
name = truncate.Truncator(name, 100, truncate.EllipsisMiddleStrategy{})
}

return name
}

func GenerateDefaultName(cronJob *v1.CronJob) string {
name := fmt.Sprintf("%s/%s", cronJob.Namespace, cronJob.Name)
return truncateDefaultName(name)
}

func ValidateTagName(tagName string) string {
name := tagName
if len(tagName) > 100 {
Expand All @@ -70,8 +64,6 @@ func ValidateTagName(tagName string) string {
func convertCronJobToCronitorJob(cronJob *v1.CronJob) CronitorJob {
configParser := pkg.NewCronitorConfigParser(cronJob)

name := GenerateDefaultName(cronJob)

metadata := make(map[string]string)
if cronJob.Spec.ConcurrencyPolicy != "" {
metadata["concurrencyPolicy"] = string(cronJob.Spec.ConcurrencyPolicy)
Expand All @@ -91,7 +83,7 @@ func convertCronJobToCronitorJob(cronJob *v1.CronJob) CronitorJob {

cronitorJob := CronitorJob{
Key: configParser.GetCronitorID(),
DefaultName: name,
Name: truncateName(configParser.GetCronitorName()),
Schedule: cronJob.Spec.Schedule,
Metadata: string(metadataJson),
Type_: "job",
Expand All @@ -100,10 +92,6 @@ func convertCronJobToCronitorJob(cronJob *v1.CronJob) CronitorJob {
Group: configParser.GetGroup(),
}

if name := configParser.GetSpecifiedCronitorName(); name != "" {
cronitorJob.Name = name
}

if graceSeconds := configParser.GetGraceSeconds(); graceSeconds != -1 {
cronitorJob.GraceSeconds = graceSeconds
}
Expand Down
Loading
Loading