refactor(workload): centralize workload listing with registry-based listers and add Argo Rollouts support

This commit is contained in:
TheiLLeniumStudios
2025-12-28 08:47:56 +01:00
parent c19058a66e
commit 3cf0119748
10 changed files with 808 additions and 123 deletions
+3 -22
View File
@@ -2,6 +2,7 @@
package config
import (
"strings"
"time"
"k8s.io/apimachinery/pkg/labels"
@@ -157,7 +158,7 @@ func DefaultAnnotations() AnnotationConfig {
// IsResourceIgnored checks if a resource name should be ignored (case-insensitive).
func (c *Config) IsResourceIgnored(name string) bool {
for _, ignored := range c.IgnoredResources {
if equalFold(ignored, name) {
if strings.EqualFold(ignored, name) {
return true
}
}
@@ -167,7 +168,7 @@ func (c *Config) IsResourceIgnored(name string) bool {
// IsWorkloadIgnored checks if a workload type should be ignored (case-insensitive).
func (c *Config) IsWorkloadIgnored(workloadType string) bool {
for _, ignored := range c.IgnoredWorkloads {
if equalFold(ignored, workloadType) {
if strings.EqualFold(ignored, workloadType) {
return true
}
}
@@ -184,23 +185,3 @@ func (c *Config) IsNamespaceIgnored(namespace string) bool {
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
}
-26
View File
@@ -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)
}
},
)
}
}
+10 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/stakater/Reloader/internal/pkg/workload"
"k8s.io/apimachinery/pkg/labels"
)
@@ -102,8 +103,16 @@ func (c *Config) Validate() error {
// Normalize IgnoredResources to lowercase for consistent comparison
c.IgnoredResources = normalizeToLower(c.IgnoredResources)
// Normalize IgnoredWorkloads to lowercase
// Validate and normalize 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 {
return errs
+14
View File
@@ -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) {
cfg := NewDefault()
cfg.ReloadStrategy = "invalid"
@@ -26,7 +26,7 @@ type DeploymentReconciler struct {
// Reconcile handles Deployment pause expiration.
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
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
if err := r.Get(ctx, req.NamespacedName, &deploy); err != nil {
+184
View File
@@ -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
}
+194
View File
@@ -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")
}
}
+283
View File
@@ -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")
}
}
+76 -57
View File
@@ -3,6 +3,7 @@ package workload
import (
"context"
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
"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) {
switch kind {
case KindDeployment:
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:
lister := l.Registry.ListerFor(kind)
if lister == 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
}
+43 -16
View File
@@ -1,7 +1,9 @@
package workload
import (
"context"
"fmt"
"strings"
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
@@ -9,16 +11,36 @@ import (
"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.
type Registry struct {
argoRolloutsEnabled bool
listers map[Kind]WorkloadLister
}
// NewRegistry creates a new workload registry.
func NewRegistry(argoRolloutsEnabled bool) *Registry {
return &Registry{
r := &Registry{
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.
@@ -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.
func KindFromString(s string) (Kind, error) {
switch s {
case "Deployment", "deployment", "deployments":
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)
if k, ok := kindAliases[strings.ToLower(s)]; ok {
return k, nil
}
return "", fmt.Errorf("unknown workload kind: %s", s)
}