Skip to content

Commit

Permalink
refresh argocd apps on exit (#341)
Browse files Browse the repository at this point in the history
Signed-off-by: Manabu McCloskey <[email protected]>
  • Loading branch information
nabuskey authored Jul 25, 2024
1 parent 1fc6f1d commit f44f162
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 6 deletions.
111 changes: 111 additions & 0 deletions pkg/controllers/localbuild/argo_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
package localbuild

import (
"context"
"testing"

argov1alpha1 "github.com/cnoe-io/argocd-api/api/argo/application/v1alpha1"
"github.com/cnoe-io/idpbuilder/api/v1alpha1"
"github.com/cnoe-io/idpbuilder/pkg/k8s"
"github.com/cnoe-io/idpbuilder/pkg/util"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type fakeKubeClient struct {
mock.Mock
client.Client
}

func (f *fakeKubeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
args := f.Called(ctx, list, opts)
return args.Error(0)
}

func (f *fakeKubeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
args := f.Called(ctx, obj, patch, opts)
return args.Error(0)
}

type testCase struct {
err error
listApps []argov1alpha1.Application
annotations []map[string]string
}

func TestGetRawInstallResources(t *testing.T) {
e := EmbeddedInstallation{
resourceFS: installArgoFS,
Expand Down Expand Up @@ -53,3 +83,84 @@ func TestGetK8sInstallResources(t *testing.T) {
t.Fatalf("Expected 58 Argo Install Resources, got: %d", len(objs))
}
}

func TestArgoCDAppAnnotation(t *testing.T) {
ctx := context.Background()

cases := []testCase{
{
err: nil,
listApps: []argov1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: argov1alpha1.ApplicationSchemaGroupVersionKind.Kind,
APIVersion: argov1alpha1.ApplicationSchemaGroupVersionKind.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "nil-annotation",
Namespace: "argocd",
},
},
},
annotations: []map[string]string{
{
argoCDApplicationAnnotationKeyRefresh: argoCDApplicationAnnotationValueRefreshNormal,
},
},
},
{
err: nil,
listApps: []argov1alpha1.Application{
{
TypeMeta: metav1.TypeMeta{
Kind: argov1alpha1.ApplicationSchemaGroupVersionKind.Kind,
APIVersion: argov1alpha1.ApplicationSchemaGroupVersionKind.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "existing-annotation",
Namespace: "argocd",
Annotations: map[string]string{
"test": "value",
},
},
},
},
annotations: []map[string]string{
{
"test": "value",
argoCDApplicationAnnotationKeyRefresh: argoCDApplicationAnnotationValueRefreshNormal,
},
},
},
}

for i := range cases {
c := cases[i]
fClient := new(fakeKubeClient)
fClient.On("List", ctx, mock.Anything, []client.ListOption{client.InNamespace(argocdNamespace)}).
Run(func(args mock.Arguments) {
apps := args.Get(1).(*argov1alpha1.ApplicationList)
apps.Items = c.listApps
}).Return(c.err)
for j := range c.annotations {
app := c.listApps[j]
u := makeUnstructured(app.Name, app.Namespace, app.GroupVersionKind(), c.annotations[j])
fClient.On("Patch", ctx, u, client.Apply, []client.PatchOption{client.FieldOwner(v1alpha1.FieldManager)}).Return(nil)
}
rec := LocalbuildReconciler{
Client: fClient,
}
err := rec.requestArgoCDAppRefresh(ctx)
fClient.AssertExpectations(t)
assert.NilError(t, err)
}
}

func makeUnstructured(name, namespace string, gvk schema.GroupVersionKind, annotations map[string]string) *unstructured.Unstructured {
u := &unstructured.Unstructured{}
u.SetAnnotations(annotations)
u.SetName(name)
u.SetNamespace(namespace)
u.SetGroupVersionKind(gvk)
return u
}
78 changes: 74 additions & 4 deletions pkg/controllers/localbuild/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const (
defaultArgoCDProjectName string = "default"
defaultRequeueTime = time.Second * 15
errRequeueTime = time.Second * 5

argoCDApplicationAnnotationKeyRefresh = "argocd.argoproj.io/refresh"
argoCDApplicationAnnotationValueRefreshNormal = "normal"
argoCDApplicationSetAnnotationKeyRefresh = "argocd.argoproj.io/application-set-refresh"
argoCDApplicationSetAnnotationKeyRefreshTrue = "true"
)

type LocalbuildReconciler struct {
Expand Down Expand Up @@ -122,18 +127,26 @@ func (r *LocalbuildReconciler) installCorePackages(ctx context.Context, req ctrl

// Responsible to updating ObservedGeneration in status
func (r *LocalbuildReconciler) postProcessReconcile(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) {
log := log.FromContext(ctx)
logger := log.FromContext(ctx)

log.Info("Checking if we should shutdown")
logger.Info("Checking if we should shutdown")
if r.shouldShutdown {
log.Info("Shutting Down")
logger.Info("Shutting Down")
err := r.requestArgoCDAppRefresh(ctx)
if err != nil {
logger.V(1).Info("failed requesting argocd application refresh", "error", err)
}
err = r.requestArgoCDAppSetRefresh(ctx)
if err != nil {
logger.V(1).Info("failed requesting argocd application set refresh", "error", err)
}
r.CancelFunc()
return
}

resource.Status.ObservedGeneration = resource.GetGeneration()
if err := r.Status().Update(ctx, resource); err != nil {
log.Error(err, "Failed to update resource status after reconcile")
logger.Error(err, "Failed to update resource status after reconcile")
}
}

Expand Down Expand Up @@ -527,6 +540,63 @@ func (r *LocalbuildReconciler) reconcileGitRepo(ctx context.Context, resource *v
return repo, err
}

func (r *LocalbuildReconciler) requestArgoCDAppRefresh(ctx context.Context) error {
apps := &argov1alpha1.ApplicationList{}
err := r.Client.List(ctx, apps, client.InNamespace(argocdNamespace))
if err != nil {
return fmt.Errorf("listing argocd apps for refresh: %w", err)
}

for i := range apps.Items {
app := apps.Items[i]
aErr := r.applyArgoCDAnnotation(ctx, &app, argocdapp.ApplicationKind, argoCDApplicationAnnotationKeyRefresh, argoCDApplicationAnnotationValueRefreshNormal)
if aErr != nil {
return aErr
}
}
return nil
}

func (r *LocalbuildReconciler) requestArgoCDAppSetRefresh(ctx context.Context) error {
appsets := &argov1alpha1.ApplicationSetList{}
err := r.Client.List(ctx, appsets, client.InNamespace(argocdNamespace))
if err != nil {
return fmt.Errorf("listing argocd apps for refresh: %w", err)
}

for i := range appsets.Items {
appset := appsets.Items[i]
aErr := r.applyArgoCDAnnotation(ctx, &appset, argocdapp.ApplicationSetKind, argoCDApplicationSetAnnotationKeyRefresh, argoCDApplicationSetAnnotationKeyRefreshTrue)
if aErr != nil {
return aErr
}
}
return nil
}

func (r *LocalbuildReconciler) applyArgoCDAnnotation(ctx context.Context, obj client.Object, argoCDType, annotationKey, annotationValue string) error {
annotations := obj.GetAnnotations()
if annotations != nil {
_, ok := annotations[annotationKey]
if !ok {
annotations[annotationKey] = annotationValue
err := util.ApplyAnnotation(ctx, r.Client, obj, annotations, client.FieldOwner(v1alpha1.FieldManager))
if err != nil {
return fmt.Errorf("applying %s refresh annotation for %s: %w", argoCDType, obj.GetName(), err)
}
}
} else {
a := map[string]string{
annotationKey: annotationValue,
}
err := util.ApplyAnnotation(ctx, r.Client, obj, a, client.FieldOwner(v1alpha1.FieldManager))
if err != nil {
return fmt.Errorf("applying %s refresh annotation for %s: %w", argoCDType, obj.GetName(), err)
}
}
return nil
}

func getCustomPackageName(fileName, appName string) string {
s := strings.Split(fileName, ".")
return fmt.Sprintf("%s-%s", strings.ToLower(s[0]), appName)
Expand Down
8 changes: 6 additions & 2 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,18 @@ func UpdateSyncAnnotation(ctx context.Context, kubeClient client.Client, obj cli
}
annotations := make(map[string]string, 1)
SetLastObservedSyncTimeAnnotationValue(annotations, timeStamp)

return ApplyAnnotation(ctx, kubeClient, obj, annotations, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager))
}

func ApplyAnnotation(ctx context.Context, kubeClient client.Client, obj client.Object, annotations map[string]string, opts ...client.PatchOption) error {
// MUST be unstructured to avoid managing fields we do not care about.
u := unstructured.Unstructured{}
u.SetAnnotations(annotations)
u.SetName(obj.GetName())
u.SetNamespace(obj.GetNamespace())
u.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind())

return kubeClient.Patch(ctx, &u, client.Apply, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager))
return kubeClient.Patch(ctx, &u, client.Apply, opts...)
}

func GeneratePassword() (string, error) {
Expand Down

0 comments on commit f44f162

Please sign in to comment.