mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-23 22:16:45 +00:00
refactor(workload): centralize workload listing with registry-based listers and add Argo Rollouts support
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
@@ -157,7 +158,7 @@ func DefaultAnnotations() AnnotationConfig {
|
|||||||
// IsResourceIgnored checks if a resource name should be ignored (case-insensitive).
|
// IsResourceIgnored checks if a resource name should be ignored (case-insensitive).
|
||||||
func (c *Config) IsResourceIgnored(name string) bool {
|
func (c *Config) IsResourceIgnored(name string) bool {
|
||||||
for _, ignored := range c.IgnoredResources {
|
for _, ignored := range c.IgnoredResources {
|
||||||
if equalFold(ignored, name) {
|
if strings.EqualFold(ignored, name) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +168,7 @@ func (c *Config) IsResourceIgnored(name string) bool {
|
|||||||
// IsWorkloadIgnored checks if a workload type should be ignored (case-insensitive).
|
// IsWorkloadIgnored checks if a workload type should be ignored (case-insensitive).
|
||||||
func (c *Config) IsWorkloadIgnored(workloadType string) bool {
|
func (c *Config) IsWorkloadIgnored(workloadType string) bool {
|
||||||
for _, ignored := range c.IgnoredWorkloads {
|
for _, ignored := range c.IgnoredWorkloads {
|
||||||
if equalFold(ignored, workloadType) {
|
if strings.EqualFold(ignored, workloadType) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,23 +185,3 @@ func (c *Config) IsNamespaceIgnored(namespace string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func equalFold(s, t string) bool {
|
|
||||||
if len(s) != len(t) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
c1, c2 := s[i], t[i]
|
|
||||||
if c1 != c2 {
|
|
||||||
if 'A' <= c1 && c1 <= 'Z' {
|
|
||||||
c1 += 'a' - 'A'
|
|
||||||
}
|
|
||||||
if 'A' <= c2 && c2 <= 'Z' {
|
|
||||||
c2 += 'a' - 'A'
|
|
||||||
}
|
|
||||||
if c1 != c2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -201,29 +201,3 @@ func TestConfig_IsNamespaceIgnored(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEqualFold(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
s, t string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"abc", "abc", true},
|
|
||||||
{"ABC", "abc", true},
|
|
||||||
{"abc", "ABC", true},
|
|
||||||
{"aBc", "AbC", true},
|
|
||||||
{"abc", "abcd", false},
|
|
||||||
{"", "", true},
|
|
||||||
{"a", "", false},
|
|
||||||
{"", "a", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(
|
|
||||||
tt.s+"_"+tt.t, func(t *testing.T) {
|
|
||||||
got := equalFold(tt.s, tt.t)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("equalFold(%q, %q) = %v, want %v", tt.s, tt.t, got, tt.want)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,8 +103,16 @@ func (c *Config) Validate() error {
|
|||||||
// Normalize IgnoredResources to lowercase for consistent comparison
|
// Normalize IgnoredResources to lowercase for consistent comparison
|
||||||
c.IgnoredResources = normalizeToLower(c.IgnoredResources)
|
c.IgnoredResources = normalizeToLower(c.IgnoredResources)
|
||||||
|
|
||||||
// Normalize IgnoredWorkloads to lowercase
|
// Validate and normalize IgnoredWorkloads
|
||||||
c.IgnoredWorkloads = normalizeToLower(c.IgnoredWorkloads)
|
c.IgnoredWorkloads = normalizeToLower(c.IgnoredWorkloads)
|
||||||
|
for _, w := range c.IgnoredWorkloads {
|
||||||
|
if _, err := workload.KindFromString(w); err != nil {
|
||||||
|
errs = append(errs, ValidationError{
|
||||||
|
Field: "IgnoredWorkloads",
|
||||||
|
Message: fmt.Sprintf("unknown workload type %q", w),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return errs
|
return errs
|
||||||
|
|||||||
@@ -178,6 +178,20 @@ func TestConfig_Validate_NormalizesIgnoredWorkloads(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_InvalidIgnoredWorkload(t *testing.T) {
|
||||||
|
cfg := NewDefault()
|
||||||
|
cfg.IgnoredWorkloads = []string{"deployment", "invalidtype"}
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Validate() should return error for invalid workload type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), "invalidtype") {
|
||||||
|
t.Errorf("Error should mention invalid workload type, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_Validate_MultipleErrors(t *testing.T) {
|
func TestConfig_Validate_MultipleErrors(t *testing.T) {
|
||||||
cfg := NewDefault()
|
cfg := NewDefault()
|
||||||
cfg.ReloadStrategy = "invalid"
|
cfg.ReloadStrategy = "invalid"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type DeploymentReconciler struct {
|
|||||||
// Reconcile handles Deployment pause expiration.
|
// Reconcile handles Deployment pause expiration.
|
||||||
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
log := r.Log.WithValues("deployment", req.NamespacedName)
|
log := r.Log.WithValues("deployment", req.NamespacedName)
|
||||||
log.Info("Deployment reconciling ", "namespace", req.Namespace, "name", req.Name)
|
log.V(1).Info("reconciling deployment", "namespace", req.Namespace, "name", req.Name)
|
||||||
|
|
||||||
var deploy appsv1.Deployment
|
var deploy appsv1.Deployment
|
||||||
if err := r.Get(ctx, req.NamespacedName, &deploy); err != nil {
|
if err := r.Get(ctx, req.NamespacedName, &deploy); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/client-go/tools/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRecorder_NilInput(t *testing.T) {
|
||||||
|
r := NewRecorder(nil)
|
||||||
|
if r != nil {
|
||||||
|
t.Error("NewRecorder(nil) should return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRecorder_ValidInput(t *testing.T) {
|
||||||
|
fakeRecorder := record.NewFakeRecorder(10)
|
||||||
|
r := NewRecorder(fakeRecorder)
|
||||||
|
if r == nil {
|
||||||
|
t.Error("NewRecorder with valid recorder should not return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadSuccess_RecordsEvent(t *testing.T) {
|
||||||
|
fakeRecorder := record.NewFakeRecorder(10)
|
||||||
|
r := NewRecorder(fakeRecorder)
|
||||||
|
|
||||||
|
pod := &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ReloadSuccess(pod, "ConfigMap", "my-config")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-fakeRecorder.Events:
|
||||||
|
if event == "" {
|
||||||
|
t.Error("Expected event to be recorded")
|
||||||
|
}
|
||||||
|
// Event format: "Normal Reloaded Reloaded due to ConfigMap my-config change"
|
||||||
|
expectedContains := []string{"Normal", "Reloaded", "ConfigMap", "my-config"}
|
||||||
|
for _, expected := range expectedContains {
|
||||||
|
if !contains(event, expected) {
|
||||||
|
t.Errorf("Event %q should contain %q", event, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Error("Expected event to be recorded, but none was")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadFailed_RecordsWarningEvent(t *testing.T) {
|
||||||
|
fakeRecorder := record.NewFakeRecorder(10)
|
||||||
|
r := NewRecorder(fakeRecorder)
|
||||||
|
|
||||||
|
pod := &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testErr := errors.New("update conflict")
|
||||||
|
r.ReloadFailed(pod, "Secret", "my-secret", testErr)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-fakeRecorder.Events:
|
||||||
|
if event == "" {
|
||||||
|
t.Error("Expected event to be recorded")
|
||||||
|
}
|
||||||
|
// Event format: "Warning ReloadFailed Failed to reload due to Secret my-secret change: update conflict"
|
||||||
|
expectedContains := []string{"Warning", "ReloadFailed", "Secret", "my-secret", "update conflict"}
|
||||||
|
for _, expected := range expectedContains {
|
||||||
|
if !contains(event, expected) {
|
||||||
|
t.Errorf("Event %q should contain %q", event, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Error("Expected event to be recorded, but none was")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilRecorder_NoPanic(t *testing.T) {
|
||||||
|
var r *Recorder = nil
|
||||||
|
|
||||||
|
pod := &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// These should not panic
|
||||||
|
r.ReloadSuccess(pod, "ConfigMap", "my-config")
|
||||||
|
r.ReloadFailed(pod, "Secret", "my-secret", errors.New("test error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecorder_NilInternalRecorder(t *testing.T) {
|
||||||
|
// Create a Recorder with nil internal recorder (edge case)
|
||||||
|
r := &Recorder{recorder: nil}
|
||||||
|
|
||||||
|
pod := &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// These should not panic
|
||||||
|
r.ReloadSuccess(pod, "ConfigMap", "my-config")
|
||||||
|
r.ReloadFailed(pod, "Secret", "my-secret", errors.New("test error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventConstants(t *testing.T) {
|
||||||
|
if EventTypeNormal != corev1.EventTypeNormal {
|
||||||
|
t.Errorf("EventTypeNormal = %q, want %q", EventTypeNormal, corev1.EventTypeNormal)
|
||||||
|
}
|
||||||
|
if EventTypeWarning != corev1.EventTypeWarning {
|
||||||
|
t.Errorf("EventTypeWarning = %q, want %q", EventTypeWarning, corev1.EventTypeWarning)
|
||||||
|
}
|
||||||
|
if ReasonReloaded != "Reloaded" {
|
||||||
|
t.Errorf("ReasonReloaded = %q, want %q", ReasonReloaded, "Reloaded")
|
||||||
|
}
|
||||||
|
if ReasonReloadFailed != "ReloadFailed" {
|
||||||
|
t.Errorf("ReasonReloadFailed = %q, want %q", ReasonReloadFailed, "ReloadFailed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadSuccess_DifferentObjectTypes(t *testing.T) {
|
||||||
|
fakeRecorder := record.NewFakeRecorder(10)
|
||||||
|
r := NewRecorder(fakeRecorder)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
object runtime.Object
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Pod",
|
||||||
|
object: &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "default"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ConfigMap",
|
||||||
|
object: &corev1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-cm", Namespace: "default"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r.ReloadSuccess(tt.object, "ConfigMap", "my-config")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-fakeRecorder.Events:
|
||||||
|
if event == "" {
|
||||||
|
t.Error("Expected event to be recorded")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Error("Expected event to be recorded")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSubstring(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCollectors_CreatesCounters(t *testing.T) {
|
||||||
|
collectors := NewCollectors()
|
||||||
|
|
||||||
|
if collectors.Reloaded == nil {
|
||||||
|
t.Error("NewCollectors() should create Reloaded counter")
|
||||||
|
}
|
||||||
|
if collectors.ReloadedByNamespace == nil {
|
||||||
|
t.Error("NewCollectors() should create ReloadedByNamespace counter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCollectors_InitializesWithZero(t *testing.T) {
|
||||||
|
collectors := NewCollectors()
|
||||||
|
|
||||||
|
// Check that success=true counter is initialized to 0
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
err := collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 0 {
|
||||||
|
t.Errorf("Initial success=true counter = %v, want 0", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that success=false counter is initialized to 0
|
||||||
|
err = collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 0 {
|
||||||
|
t.Errorf("Initial success=false counter = %v, want 0", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordReload_Success(t *testing.T) {
|
||||||
|
collectors := NewCollectors()
|
||||||
|
collectors.RecordReload(true, "default")
|
||||||
|
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
err := collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 1 {
|
||||||
|
t.Errorf("success=true counter = %v, want 1", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordReload_Failure(t *testing.T) {
|
||||||
|
collectors := NewCollectors()
|
||||||
|
collectors.RecordReload(false, "default")
|
||||||
|
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
err := collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 1 {
|
||||||
|
t.Errorf("success=false counter = %v, want 1", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordReload_MultipleIncrements(t *testing.T) {
|
||||||
|
collectors := NewCollectors()
|
||||||
|
collectors.RecordReload(true, "default")
|
||||||
|
collectors.RecordReload(true, "default")
|
||||||
|
collectors.RecordReload(false, "default")
|
||||||
|
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
|
||||||
|
err := collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 2 {
|
||||||
|
t.Errorf("success=true counter = %v, want 2", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 1 {
|
||||||
|
t.Errorf("success=false counter = %v, want 1", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordReload_WithNamespaceTracking(t *testing.T) {
|
||||||
|
// Enable namespace tracking
|
||||||
|
os.Setenv("METRICS_COUNT_BY_NAMESPACE", "enabled")
|
||||||
|
defer os.Unsetenv("METRICS_COUNT_BY_NAMESPACE")
|
||||||
|
|
||||||
|
collectors := NewCollectors()
|
||||||
|
collectors.RecordReload(true, "kube-system")
|
||||||
|
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
err := collectors.ReloadedByNamespace.With(prometheus.Labels{
|
||||||
|
"success": "true",
|
||||||
|
"namespace": "kube-system",
|
||||||
|
}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 1 {
|
||||||
|
t.Errorf("namespace counter = %v, want 1", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordReload_WithoutNamespaceTracking(t *testing.T) {
|
||||||
|
// Ensure namespace tracking is disabled
|
||||||
|
os.Unsetenv("METRICS_COUNT_BY_NAMESPACE")
|
||||||
|
|
||||||
|
collectors := NewCollectors()
|
||||||
|
collectors.RecordReload(true, "kube-system")
|
||||||
|
|
||||||
|
// The ReloadedByNamespace counter should not be incremented
|
||||||
|
// We can verify by checking countByNamespace is false
|
||||||
|
if collectors.countByNamespace {
|
||||||
|
t.Error("countByNamespace should be false when env var is not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilCollectors_NoPanic(t *testing.T) {
|
||||||
|
var c *Collectors = nil
|
||||||
|
|
||||||
|
// This should not panic
|
||||||
|
c.RecordReload(true, "default")
|
||||||
|
c.RecordReload(false, "default")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordReload_DifferentNamespaces(t *testing.T) {
|
||||||
|
os.Setenv("METRICS_COUNT_BY_NAMESPACE", "enabled")
|
||||||
|
defer os.Unsetenv("METRICS_COUNT_BY_NAMESPACE")
|
||||||
|
|
||||||
|
collectors := NewCollectors()
|
||||||
|
collectors.RecordReload(true, "namespace-a")
|
||||||
|
collectors.RecordReload(true, "namespace-b")
|
||||||
|
collectors.RecordReload(true, "namespace-a")
|
||||||
|
|
||||||
|
metric := &dto.Metric{}
|
||||||
|
|
||||||
|
// Check namespace-a has 2 reloads
|
||||||
|
err := collectors.ReloadedByNamespace.With(prometheus.Labels{
|
||||||
|
"success": "true",
|
||||||
|
"namespace": "namespace-a",
|
||||||
|
}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 2 {
|
||||||
|
t.Errorf("namespace-a counter = %v, want 2", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check namespace-b has 1 reload
|
||||||
|
err = collectors.ReloadedByNamespace.With(prometheus.Labels{
|
||||||
|
"success": "true",
|
||||||
|
"namespace": "namespace-b",
|
||||||
|
}).Write(metric)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get metric: %v", err)
|
||||||
|
}
|
||||||
|
if metric.Counter.GetValue() != 1 {
|
||||||
|
t.Errorf("namespace-b counter = %v, want 1", metric.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectors_MetricNames(t *testing.T) {
|
||||||
|
collectors := NewCollectors()
|
||||||
|
|
||||||
|
// Verify the Reloaded metric has correct description
|
||||||
|
ch := make(chan *prometheus.Desc, 10)
|
||||||
|
collectors.Reloaded.Describe(ch)
|
||||||
|
close(ch)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for desc := range ch {
|
||||||
|
if desc.String() != "" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("Expected Reloaded metric to have a description")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewClient_SetsURL(t *testing.T) {
|
||||||
|
c := NewClient("http://example.com/webhook", logr.Discard())
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
t.Fatal("NewClient should not return nil")
|
||||||
|
}
|
||||||
|
if c.url != "http://example.com/webhook" {
|
||||||
|
t.Errorf("URL = %q, want %q", c.url, "http://example.com/webhook")
|
||||||
|
}
|
||||||
|
if c.httpClient == nil {
|
||||||
|
t.Error("httpClient should not be nil")
|
||||||
|
}
|
||||||
|
if c.httpClient.Timeout != 30*time.Second {
|
||||||
|
t.Errorf("Timeout = %v, want %v", c.httpClient.Timeout, 30*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsConfigured_NilClient(t *testing.T) {
|
||||||
|
var c *Client = nil
|
||||||
|
|
||||||
|
if c.IsConfigured() {
|
||||||
|
t.Error("IsConfigured() should return false for nil client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsConfigured_EmptyURL(t *testing.T) {
|
||||||
|
c := NewClient("", logr.Discard())
|
||||||
|
|
||||||
|
if c.IsConfigured() {
|
||||||
|
t.Error("IsConfigured() should return false for empty URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsConfigured_ValidURL(t *testing.T) {
|
||||||
|
c := NewClient("http://example.com/webhook", logr.Discard())
|
||||||
|
|
||||||
|
if !c.IsConfigured() {
|
||||||
|
t.Error("IsConfigured() should return true for valid URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_EmptyURL_ReturnsNil(t *testing.T) {
|
||||||
|
c := NewClient("", logr.Discard())
|
||||||
|
|
||||||
|
payload := Payload{
|
||||||
|
Kind: "ConfigMap",
|
||||||
|
Namespace: "default",
|
||||||
|
ResourceName: "my-config",
|
||||||
|
ResourceType: "configmap",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Send(context.Background(), payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Send() with empty URL should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_MarshalPayload(t *testing.T) {
|
||||||
|
var receivedPayload Payload
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
json.Unmarshal(body, &receivedPayload)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL, logr.Discard())
|
||||||
|
|
||||||
|
payload := Payload{
|
||||||
|
Kind: "ConfigMap",
|
||||||
|
Namespace: "default",
|
||||||
|
ResourceName: "my-config",
|
||||||
|
ResourceType: "configmap",
|
||||||
|
Hash: "abc123",
|
||||||
|
Timestamp: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||||
|
Workloads: []WorkloadInfo{
|
||||||
|
{Kind: "Deployment", Name: "my-deploy", Namespace: "default"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Send(context.Background(), payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Send() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedPayload.Kind != "ConfigMap" {
|
||||||
|
t.Errorf("Received Kind = %q, want %q", receivedPayload.Kind, "ConfigMap")
|
||||||
|
}
|
||||||
|
if receivedPayload.Namespace != "default" {
|
||||||
|
t.Errorf("Received Namespace = %q, want %q", receivedPayload.Namespace, "default")
|
||||||
|
}
|
||||||
|
if receivedPayload.ResourceName != "my-config" {
|
||||||
|
t.Errorf("Received ResourceName = %q, want %q", receivedPayload.ResourceName, "my-config")
|
||||||
|
}
|
||||||
|
if receivedPayload.Hash != "abc123" {
|
||||||
|
t.Errorf("Received Hash = %q, want %q", receivedPayload.Hash, "abc123")
|
||||||
|
}
|
||||||
|
if len(receivedPayload.Workloads) != 1 {
|
||||||
|
t.Errorf("Received Workloads count = %d, want 1", len(receivedPayload.Workloads))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_SetsCorrectHeaders(t *testing.T) {
|
||||||
|
var contentType, userAgent string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
contentType = r.Header.Get("Content-Type")
|
||||||
|
userAgent = r.Header.Get("User-Agent")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL, logr.Discard())
|
||||||
|
|
||||||
|
err := c.Send(context.Background(), Payload{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Send() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType != "application/json" {
|
||||||
|
t.Errorf("Content-Type = %q, want %q", contentType, "application/json")
|
||||||
|
}
|
||||||
|
if userAgent != "Reloader/2.0" {
|
||||||
|
t.Errorf("User-Agent = %q, want %q", userAgent, "Reloader/2.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_UsesPostMethod(t *testing.T) {
|
||||||
|
var method string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
method = r.Method
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL, logr.Discard())
|
||||||
|
|
||||||
|
err := c.Send(context.Background(), Payload{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Send() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if method != http.MethodPost {
|
||||||
|
t.Errorf("Method = %q, want %q", method, http.MethodPost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_Non2xxResponse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
statusCode int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"200 OK", 200, false},
|
||||||
|
{"201 Created", 201, false},
|
||||||
|
{"204 No Content", 204, false},
|
||||||
|
{"299 upper bound", 299, false},
|
||||||
|
{"300 redirect", 300, true},
|
||||||
|
{"400 Bad Request", 400, true},
|
||||||
|
{"404 Not Found", 404, true},
|
||||||
|
{"500 Internal Error", 500, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(tt.statusCode)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL, logr.Discard())
|
||||||
|
err := c.Send(context.Background(), Payload{})
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Send() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_NetworkError(t *testing.T) {
|
||||||
|
// Use a URL that won't connect
|
||||||
|
c := NewClient("http://127.0.0.1:1", logr.Discard())
|
||||||
|
|
||||||
|
err := c.Send(context.Background(), Payload{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Send() should return error for network failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_ContextCancellation(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := NewClient(server.URL, logr.Discard())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // Cancel immediately
|
||||||
|
|
||||||
|
err := c.Send(ctx, Payload{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Send() should return error for cancelled context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayload_JSONSerialization(t *testing.T) {
|
||||||
|
payload := Payload{
|
||||||
|
Kind: "ConfigMap",
|
||||||
|
Namespace: "default",
|
||||||
|
ResourceName: "my-config",
|
||||||
|
ResourceType: "configmap",
|
||||||
|
Hash: "abc123",
|
||||||
|
Timestamp: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||||
|
Workloads: []WorkloadInfo{
|
||||||
|
{Kind: "Deployment", Name: "my-deploy", Namespace: "default"},
|
||||||
|
{Kind: "StatefulSet", Name: "my-sts", Namespace: "default"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshaled Payload
|
||||||
|
if err := json.Unmarshal(data, &unmarshaled); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaled.Kind != payload.Kind {
|
||||||
|
t.Errorf("Kind = %q, want %q", unmarshaled.Kind, payload.Kind)
|
||||||
|
}
|
||||||
|
if len(unmarshaled.Workloads) != 2 {
|
||||||
|
t.Errorf("Workloads count = %d, want 2", len(unmarshaled.Workloads))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkloadInfo_JSONSerialization(t *testing.T) {
|
||||||
|
info := WorkloadInfo{
|
||||||
|
Kind: "Deployment",
|
||||||
|
Name: "my-deploy",
|
||||||
|
Namespace: "production",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshaled WorkloadInfo
|
||||||
|
if err := json.Unmarshal(data, &unmarshaled); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaled.Kind != "Deployment" {
|
||||||
|
t.Errorf("Kind = %q, want %q", unmarshaled.Kind, "Deployment")
|
||||||
|
}
|
||||||
|
if unmarshaled.Name != "my-deploy" {
|
||||||
|
t.Errorf("Name = %q, want %q", unmarshaled.Name, "my-deploy")
|
||||||
|
}
|
||||||
|
if unmarshaled.Namespace != "production" {
|
||||||
|
t.Errorf("Namespace = %q, want %q", unmarshaled.Namespace, "production")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package workload
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
@@ -49,63 +50,81 @@ func (l *Lister) List(ctx context.Context, namespace string) ([]WorkloadAccessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Lister) listByKind(ctx context.Context, namespace string, kind Kind) ([]WorkloadAccessor, error) {
|
func (l *Lister) listByKind(ctx context.Context, namespace string, kind Kind) ([]WorkloadAccessor, error) {
|
||||||
switch kind {
|
lister := l.Registry.ListerFor(kind)
|
||||||
case KindDeployment:
|
if lister == nil {
|
||||||
var list appsv1.DeploymentList
|
|
||||||
if err := l.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]WorkloadAccessor, len(list.Items))
|
|
||||||
for i := range list.Items {
|
|
||||||
result[i] = NewDeploymentWorkload(&list.Items[i])
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
case KindDaemonSet:
|
|
||||||
var list appsv1.DaemonSetList
|
|
||||||
if err := l.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]WorkloadAccessor, len(list.Items))
|
|
||||||
for i := range list.Items {
|
|
||||||
result[i] = NewDaemonSetWorkload(&list.Items[i])
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
case KindStatefulSet:
|
|
||||||
var list appsv1.StatefulSetList
|
|
||||||
if err := l.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]WorkloadAccessor, len(list.Items))
|
|
||||||
for i := range list.Items {
|
|
||||||
result[i] = NewStatefulSetWorkload(&list.Items[i])
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
case KindJob:
|
|
||||||
var list batchv1.JobList
|
|
||||||
if err := l.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]WorkloadAccessor, len(list.Items))
|
|
||||||
for i := range list.Items {
|
|
||||||
result[i] = NewJobWorkload(&list.Items[i])
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
case KindCronJob:
|
|
||||||
var list batchv1.CronJobList
|
|
||||||
if err := l.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]WorkloadAccessor, len(list.Items))
|
|
||||||
for i := range list.Items {
|
|
||||||
result[i] = NewCronJobWorkload(&list.Items[i])
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
return lister(ctx, l.Client, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDeployments(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) {
|
||||||
|
var list appsv1.DeploymentList
|
||||||
|
if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]WorkloadAccessor, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
result[i] = NewDeploymentWorkload(&list.Items[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDaemonSets(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) {
|
||||||
|
var list appsv1.DaemonSetList
|
||||||
|
if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]WorkloadAccessor, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
result[i] = NewDaemonSetWorkload(&list.Items[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listStatefulSets(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) {
|
||||||
|
var list appsv1.StatefulSetList
|
||||||
|
if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]WorkloadAccessor, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
result[i] = NewStatefulSetWorkload(&list.Items[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listJobs(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) {
|
||||||
|
var list batchv1.JobList
|
||||||
|
if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]WorkloadAccessor, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
result[i] = NewJobWorkload(&list.Items[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listCronJobs(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) {
|
||||||
|
var list batchv1.CronJobList
|
||||||
|
if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]WorkloadAccessor, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
result[i] = NewCronJobWorkload(&list.Items[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listRollouts(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) {
|
||||||
|
var list argorolloutv1alpha1.RolloutList
|
||||||
|
if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]WorkloadAccessor, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
result[i] = NewRolloutWorkload(&list.Items[i])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package workload
|
package workload
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
|
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
@@ -9,16 +11,36 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WorkloadLister is a function that lists workloads of a specific kind.
|
||||||
|
type WorkloadLister func(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error)
|
||||||
|
|
||||||
// Registry provides factory methods for creating Workload instances.
|
// Registry provides factory methods for creating Workload instances.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
argoRolloutsEnabled bool
|
argoRolloutsEnabled bool
|
||||||
|
listers map[Kind]WorkloadLister
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry creates a new workload registry.
|
// NewRegistry creates a new workload registry.
|
||||||
func NewRegistry(argoRolloutsEnabled bool) *Registry {
|
func NewRegistry(argoRolloutsEnabled bool) *Registry {
|
||||||
return &Registry{
|
r := &Registry{
|
||||||
argoRolloutsEnabled: argoRolloutsEnabled,
|
argoRolloutsEnabled: argoRolloutsEnabled,
|
||||||
|
listers: map[Kind]WorkloadLister{
|
||||||
|
KindDeployment: listDeployments,
|
||||||
|
KindDaemonSet: listDaemonSets,
|
||||||
|
KindStatefulSet: listStatefulSets,
|
||||||
|
KindJob: listJobs,
|
||||||
|
KindCronJob: listCronJobs,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
if argoRolloutsEnabled {
|
||||||
|
r.listers[KindArgoRollout] = listRollouts
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListerFor returns the lister function for the given kind, or nil if not found.
|
||||||
|
func (r *Registry) ListerFor(kind Kind) WorkloadLister {
|
||||||
|
return r.listers[kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportedKinds returns all supported workload kinds.
|
// SupportedKinds returns all supported workload kinds.
|
||||||
@@ -59,22 +81,27 @@ func (r *Registry) FromObject(obj client.Object) (WorkloadAccessor, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// kindAliases maps string representations to Kind constants.
|
||||||
|
// Supports lowercase, title case, and plural forms for user convenience.
|
||||||
|
var kindAliases = map[string]Kind{
|
||||||
|
"deployment": KindDeployment,
|
||||||
|
"deployments": KindDeployment,
|
||||||
|
"daemonset": KindDaemonSet,
|
||||||
|
"daemonsets": KindDaemonSet,
|
||||||
|
"statefulset": KindStatefulSet,
|
||||||
|
"statefulsets": KindStatefulSet,
|
||||||
|
"rollout": KindArgoRollout,
|
||||||
|
"rollouts": KindArgoRollout,
|
||||||
|
"job": KindJob,
|
||||||
|
"jobs": KindJob,
|
||||||
|
"cronjob": KindCronJob,
|
||||||
|
"cronjobs": KindCronJob,
|
||||||
|
}
|
||||||
|
|
||||||
// KindFromString converts a string to a Kind.
|
// KindFromString converts a string to a Kind.
|
||||||
func KindFromString(s string) (Kind, error) {
|
func KindFromString(s string) (Kind, error) {
|
||||||
switch s {
|
if k, ok := kindAliases[strings.ToLower(s)]; ok {
|
||||||
case "Deployment", "deployment", "deployments":
|
return k, nil
|
||||||
return KindDeployment, nil
|
|
||||||
case "DaemonSet", "daemonset", "daemonsets":
|
|
||||||
return KindDaemonSet, nil
|
|
||||||
case "StatefulSet", "statefulset", "statefulsets":
|
|
||||||
return KindStatefulSet, nil
|
|
||||||
case "Rollout", "rollout", "rollouts":
|
|
||||||
return KindArgoRollout, nil
|
|
||||||
case "Job", "job", "jobs":
|
|
||||||
return KindJob, nil
|
|
||||||
case "CronJob", "cronjob", "cronjobs":
|
|
||||||
return KindCronJob, nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unknown workload kind: %s", s)
|
|
||||||
}
|
}
|
||||||
|
return "", fmt.Errorf("unknown workload kind: %s", s)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user