summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorJort Koopmans <jort.koopmans@entrnce.com>2024-10-14 14:45:27 +0200
committerGitHub <noreply@github.com>2024-10-14 08:45:27 -0400
commitad9648f186538b4dd41e567ed79c720a3c241531 (patch)
treeb6ab42bc647c8b030cae5e93eb58273c6a5ff852 /pkg
parent2e631b02289acbc15c40879994eaf2fd30b4ab2c (diff)
k8sClient get resources across all namespaces (#601) (#854)
Signed-off-by: Jort Koopmans <jort.koopmans@entrnce.com>
Diffstat (limited to 'pkg')
-rw-r--r--pkg/argocd/argocd.go80
-rw-r--r--pkg/argocd/argocd_test.go191
2 files changed, 232 insertions, 39 deletions
diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go
index 4596f50..9846e7e 100644
--- a/pkg/argocd/argocd.go
+++ b/pkg/argocd/argocd.go
@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"strings"
+ "time"
"github.com/argoproj-labs/argocd-image-updater/pkg/common"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
@@ -25,40 +26,97 @@ type k8sClient struct {
kubeClient *kube.KubernetesClient
}
+// GetApplication retrieves an application by name across all namespaces.
func (client *k8sClient) GetApplication(ctx context.Context, appName string) (*v1alpha1.Application, error) {
- return client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).Get(ctx, appName, v1.GetOptions{})
+ log.Debugf("Getting application %s across all namespaces", appName)
+
+ // List all applications across all namespaces (using empty labelSelector)
+ appList, err := client.ListApplications("")
+ if err != nil {
+ return nil, fmt.Errorf("error listing applications: %w", err)
+ }
+
+ // Filter applications by name using nameMatchesPattern
+ app, err := findApplicationByName(appList, appName)
+ if err != nil {
+ log.Errorf("error getting application: %v", err)
+ return nil, fmt.Errorf("error getting application: %w", err)
+ }
+
+ // Retrieve the application in the specified namespace
+ return client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(ctx, app.Name, v1.GetOptions{})
}
+// ListApplications lists all applications across all namespaces.
func (client *k8sClient) ListApplications(labelSelector string) ([]v1alpha1.Application, error) {
- list, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).List(context.TODO(), v1.ListOptions{LabelSelector: labelSelector})
+ list, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(v1.NamespaceAll).List(context.TODO(), v1.ListOptions{LabelSelector: labelSelector})
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("error listing applications: %w", err)
}
+ log.Debugf("Applications listed: %d", len(list.Items))
return list.Items, nil
}
+// findApplicationByName filters the list of applications by name using nameMatchesPattern.
+func findApplicationByName(appList []v1alpha1.Application, appName string) (*v1alpha1.Application, error) {
+ var matchedApps []*v1alpha1.Application
+
+ for _, app := range appList {
+ log.Debugf("Found application: %s in namespace %s", app.Name, app.Namespace)
+ if nameMatchesPattern(app.Name, []string{appName}) {
+ log.Debugf("Application %s matches the pattern", app.Name)
+ matchedApps = append(matchedApps, &app)
+ }
+ }
+
+ if len(matchedApps) == 0 {
+ return nil, fmt.Errorf("application %s not found", appName)
+ }
+
+ if len(matchedApps) > 1 {
+ return nil, fmt.Errorf("multiple applications found matching %s", appName)
+ }
+
+ return matchedApps[0], nil
+}
+
func (client *k8sClient) UpdateSpec(ctx context.Context, spec *application.ApplicationUpdateSpecRequest) (*v1alpha1.ApplicationSpec, error) {
- for {
- app, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).Get(ctx, spec.GetName(), v1.GetOptions{})
+ const defaultMaxRetries = 7
+ const baseDelay = 100 * time.Millisecond // Initial delay before retrying
+
+ // Allow overriding max retries for testing purposes
+ maxRetries := defaultMaxRetries
+ if overrideRetries, ok := os.LookupEnv("OVERRIDE_MAX_RETRIES"); ok {
+ var retries int
+ if _, err := fmt.Sscanf(overrideRetries, "%d", &retries); err == nil {
+ maxRetries = retries
+ }
+ }
+
+ for attempts := 0; attempts < maxRetries; attempts++ {
+ app, err := client.GetApplication(ctx, spec.GetName())
if err != nil {
- return nil, err
+ log.Errorf("could not get application: %s, error: %v", spec.GetName(), err)
+ return nil, fmt.Errorf("error getting application: %w", err)
}
app.Spec = *spec.Spec
- updatedApp, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).Update(ctx, app, v1.UpdateOptions{})
+ updatedApp, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(app.Namespace).Update(ctx, app, v1.UpdateOptions{})
if err != nil {
if errors.IsConflict(err) {
+ log.Warnf("conflict occurred while updating application: %s, retrying... (%d/%d)", spec.GetName(), attempts+1, maxRetries)
+ time.Sleep(baseDelay * (1 << attempts)) // Exponential backoff, multiply baseDelay by 2^attempts
continue
}
- return nil, err
+ log.Errorf("could not update application: %s, error: %v", spec.GetName(), err)
+ return nil, fmt.Errorf("error updating application: %w", err)
}
return &updatedApp.Spec, nil
}
-
+ return nil, fmt.Errorf("max retries(%d) reached while updating application: %s", maxRetries, spec.GetName())
}
-// NewAPIClient creates a new API client for ArgoCD and connects to the ArgoCD
-// API server.
+// NewK8SClient creates a new kubernetes client to interact with kubernetes api-server.
func NewK8SClient(kubeClient *kube.KubernetesClient) (ArgoCD, error) {
return &k8sClient{kubeClient: kubeClient}, nil
}
diff --git a/pkg/argocd/argocd_test.go b/pkg/argocd/argocd_test.go
index 317875c..ce565f2 100644
--- a/pkg/argocd/argocd_test.go
+++ b/pkg/argocd/argocd_test.go
@@ -3,6 +3,7 @@ package argocd
import (
"context"
"fmt"
+ "os"
"testing"
"github.com/argoproj-labs/argocd-image-updater/pkg/common"
@@ -1024,59 +1025,193 @@ func TestKubernetesClient(t *testing.T) {
t.Run("List applications", func(t *testing.T) {
apps, err := client.ListApplications("")
require.NoError(t, err)
- require.Len(t, apps, 1)
-
- assert.ElementsMatch(t, []string{"test-app1"}, []string{app1.Name})
+ require.Len(t, apps, 2)
+ assert.ElementsMatch(t, []string{"test-app1", "test-app2"}, []string{app1.Name, app2.Name})
})
- t.Run("Get application successful", func(t *testing.T) {
+ t.Run("Get application test-app1 successful", func(t *testing.T) {
app, err := client.GetApplication(context.TODO(), "test-app1")
require.NoError(t, err)
assert.Equal(t, "test-app1", app.GetName())
})
+ t.Run("Get application test-app2 successful", func(t *testing.T) {
+ app, err := client.GetApplication(context.TODO(), "test-app2")
+ require.NoError(t, err)
+ assert.Equal(t, "test-app2", app.GetName())
+ })
+
t.Run("Get application not found", func(t *testing.T) {
- _, err := client.GetApplication(context.TODO(), "test-app2")
+ _, err := client.GetApplication(context.TODO(), "test-app-non-existent")
require.Error(t, err)
- assert.True(t, errors.IsNotFound(err))
+ assert.Contains(t, err.Error(), "application test-app-non-existent not found")
+ })
+
+ t.Run("List and Get applications errors", func(t *testing.T) {
+ // Create a fake clientset
+ clientset := fake.NewSimpleClientset()
+
+ // Simulate an error in the List action
+ clientset.PrependReactor("list", "applications", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
+ return true, nil, errors.NewInternalError(fmt.Errorf("list error"))
+ })
+
+ // Create the Kubernetes client
+ client, err := NewK8SClient(&kube.KubernetesClient{
+ ApplicationsClientset: clientset,
+ })
+ require.NoError(t, err)
+
+ // Test ListApplications error handling
+ apps, err := client.ListApplications("")
+ assert.Nil(t, apps)
+ assert.EqualError(t, err, "error listing applications: Internal error occurred: list error")
+
+ // Test GetApplication error handling
+ _, err = client.GetApplication(context.TODO(), "test-app")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "error listing applications: Internal error occurred: list error")
+ })
+
+ t.Run("Get applications with multiple applications found", func(t *testing.T) {
+ // Create a fake clientset with multiple applications having the same name
+ clientset := fake.NewSimpleClientset(
+ &v1alpha1.Application{
+ ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "ns1"},
+ Spec: v1alpha1.ApplicationSpec{},
+ },
+ &v1alpha1.Application{
+ ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "ns2"},
+ Spec: v1alpha1.ApplicationSpec{},
+ },
+ )
+
+ // Create the Kubernetes client
+ client, err := NewK8SClient(&kube.KubernetesClient{
+ ApplicationsClientset: clientset,
+ })
+ require.NoError(t, err)
+
+ // Test GetApplication with multiple matching applications
+ _, err = client.GetApplication(context.TODO(), "test-app")
+ assert.Error(t, err)
+ assert.EqualError(t, err, "error getting application: multiple applications found matching test-app")
})
}
-func TestKubernetesClient_UpdateSpec_Conflict(t *testing.T) {
+func TestKubernetesClientUpdateSpec(t *testing.T) {
app := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
}
clientset := fake.NewSimpleClientset(app)
- attempts := 0
- clientset.PrependReactor("update", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
- if attempts == 0 {
- attempts++
- return true, nil, errors.NewConflict(
- schema.GroupResource{Group: "argoproj.io", Resource: "Application"}, app.Name, fmt.Errorf("conflict updating %s", app.Name))
- } else {
- return false, nil, nil
- }
+ t.Run("Successful update after conflict retry", func(t *testing.T) {
+ attempts := 0
+ clientset.PrependReactor("update", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
+ if attempts == 0 {
+ attempts++
+ return true, nil, errors.NewConflict(
+ schema.GroupResource{Group: "argoproj.io", Resource: "Application"}, app.Name, fmt.Errorf("conflict updating %s", app.Name))
+ } else {
+ return false, nil, nil
+ }
+ })
+
+ client, err := NewK8SClient(&kube.KubernetesClient{
+ ApplicationsClientset: clientset,
+ })
+ require.NoError(t, err)
+
+ appName := "test-app"
+ spec, err := client.UpdateSpec(context.TODO(), &application.ApplicationUpdateSpecRequest{
+ Name: &appName,
+ Spec: &v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{
+ RepoURL: "https://github.com/argoproj/argocd-example-apps",
+ }},
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, "https://github.com/argoproj/argocd-example-apps", spec.Source.RepoURL)
})
- client, err := NewK8SClient(&kube.KubernetesClient{
- Namespace: "testns",
- ApplicationsClientset: clientset,
+ t.Run("UpdateSpec errors - application not found", func(t *testing.T) {
+ // Create a fake empty clientset
+ clientset := fake.NewSimpleClientset()
+
+ client, err := NewK8SClient(&kube.KubernetesClient{
+ ApplicationsClientset: clientset,
+ })
+ require.NoError(t, err)
+
+ appName := "test-app"
+ appNamespace := "testns"
+ spec := &application.ApplicationUpdateSpecRequest{
+ Name: &appName,
+ AppNamespace: &appNamespace,
+ Spec: &v1alpha1.ApplicationSpec{},
+ }
+
+ _, err = client.UpdateSpec(context.TODO(), spec)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "error getting application: application test-app not found")
})
- require.NoError(t, err)
- appName := "test-app"
+ t.Run("UpdateSpec errors - conflict failing retries", func(t *testing.T) {
+ clientset := fake.NewSimpleClientset(&v1alpha1.Application{
+ ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
+ Spec: v1alpha1.ApplicationSpec{},
+ })
- spec, err := client.UpdateSpec(context.TODO(), &application.ApplicationUpdateSpecRequest{
- Name: &appName,
- Spec: &v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{
- RepoURL: "https://github.com/argoproj/argocd-example-apps",
- }},
+ client, err := NewK8SClient(&kube.KubernetesClient{
+ ApplicationsClientset: clientset,
+ })
+ require.NoError(t, err)
+
+ clientset.PrependReactor("update", "applications", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
+ return true, nil, errors.NewConflict(v1alpha1.Resource("applications"), "test-app", fmt.Errorf("conflict error"))
+ })
+
+ os.Setenv("OVERRIDE_MAX_RETRIES", "0")
+ defer os.Unsetenv("OVERRIDE_MAX_RETRIES")
+
+ appName := "test-app"
+ spec := &application.ApplicationUpdateSpecRequest{
+ Name: &appName,
+ Spec: &v1alpha1.ApplicationSpec{},
+ }
+
+ _, err = client.UpdateSpec(context.TODO(), spec)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "max retries(0) reached while updating application: test-app")
})
- require.NoError(t, err)
+ t.Run("UpdateSpec errors - non-conflict update error", func(t *testing.T) {
+ clientset := fake.NewSimpleClientset(&v1alpha1.Application{
+ ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
+ Spec: v1alpha1.ApplicationSpec{},
+ })
+
+ client, err := NewK8SClient(&kube.KubernetesClient{
+ ApplicationsClientset: clientset,
+ })
+ require.NoError(t, err)
+
+ clientset.PrependReactor("update", "applications", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
+ return true, nil, fmt.Errorf("non-conflict error")
+ })
- assert.Equal(t, "https://github.com/argoproj/argocd-example-apps", spec.Source.RepoURL)
+ appName := "test-app"
+ appNamespace := "testns"
+ spec := &application.ApplicationUpdateSpecRequest{
+ Name: &appName,
+ AppNamespace: &appNamespace,
+ Spec: &v1alpha1.ApplicationSpec{},
+ }
+
+ _, err = client.UpdateSpec(context.TODO(), spec)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "error updating application: non-conflict error")
+ })
}
func Test_parseImageList(t *testing.T) {