Skip to content
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
217 changes: 204 additions & 13 deletions cmd/operator/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -215,8 +216,8 @@ func TestMain(m *testing.M) {
ObjectMeta: metav1.ObjectMeta{Name: "joy-operator-tests", Namespace: "default"},
},
k8s.WaitOptions{
Interval: 250 * time.Millisecond,
Timeout: 15 * time.Second,
Interval: 500 * time.Millisecond,
Timeout: 30 * time.Second,
},
),
)
Expand All @@ -238,7 +239,7 @@ func TestHappyReconciliations(t *testing.T) {
Resource: "projects",
})

project, err := projectIntf.Create(
project, err := projectIntf.Apply(
t.Context(),
&v1alpha1.Project{
ApiVersion: v1alpha1.GroupVersion.Identifier(),
Expand All @@ -250,7 +251,7 @@ func TestHappyReconciliations(t *testing.T) {
Repository: "org/repo",
},
},
metav1.CreateOptions{},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

Expand All @@ -260,7 +261,7 @@ func TestHappyReconciliations(t *testing.T) {
Resource: "catalogs",
})

catalog, err := catalogIntf.Create(
catalog, err := catalogIntf.Apply(
t.Context(),
&v1alpha1.Catalog{
TypeMeta: metav1.TypeMeta{
Expand All @@ -286,7 +287,7 @@ func TestHappyReconciliations(t *testing.T) {
},
},
},
metav1.CreateOptions{},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

Expand All @@ -296,7 +297,7 @@ func TestHappyReconciliations(t *testing.T) {
Resource: "environments",
})

env, err := envIntf.Create(
env, err := envIntf.Apply(
t.Context(),
&v1alpha1.Environment{
ApiVersion: v1alpha1.GroupVersion.Identifier(),
Expand All @@ -308,7 +309,7 @@ func TestHappyReconciliations(t *testing.T) {
},
},
},
metav1.CreateOptions{},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

Expand All @@ -331,7 +332,7 @@ func TestHappyReconciliations(t *testing.T) {

releaseIntf = releaseIntf.Namespace(env.Namespace)

release, err := releaseIntf.Create(
release, err := releaseIntf.Apply(
t.Context(),
&v1alpha1.Release{
Kind: v1alpha1.ReleaseGK.Kind,
Expand All @@ -352,7 +353,7 @@ func TestHappyReconciliations(t *testing.T) {
},
},
},
metav1.CreateOptions{},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

Expand Down Expand Up @@ -422,7 +423,7 @@ func TestHappyReconciliations(t *testing.T) {
},
app.Spec.Destination,
)
require.Equal(t, new(argocd.SyncPolicyAutomated), app.Spec.SyncPolicy.Automated)
require.Equal(t, &argocd.SyncPolicyAutomated{Prune: new(true)}, app.Spec.SyncPolicy.Automated)
},
"staging-test": func(t *testing.T, app *argocd.Application) {
require.Equal(t, "argocd", app.Namespace)
Expand Down Expand Up @@ -616,7 +617,7 @@ func TestEnvironmentSourcePattern(t *testing.T) {
Resource: "catalogs",
})

_, err = catalogIntf.Create(
_, err = catalogIntf.Apply(
t.Context(),
&v1alpha1.Catalog{
TypeMeta: metav1.TypeMeta{
Expand All @@ -629,7 +630,7 @@ func TestEnvironmentSourcePattern(t *testing.T) {
Revision: "main",
},
},
metav1.CreateOptions{},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

Expand Down Expand Up @@ -685,6 +686,196 @@ func TestEnvironmentSourcePattern(t *testing.T) {
)
}

func TestReleasePruning(t *testing.T) {
client, err := getKubeClient()
require.NoError(t, err)

projectIntf := k8s.TypedInterface[v1alpha1.Project](client, schema.GroupVersionResource{
Group: v1alpha1.ProjectGVK.Group,
Version: "v1alpha1",
Resource: "projects",
})

project, err := projectIntf.Apply(
t.Context(),
&v1alpha1.Project{
ApiVersion: v1alpha1.GroupVersion.Identifier(),
Kind: v1alpha1.ProjectGK.Kind,
ProjectMetadata: v1alpha1.ProjectMetadata{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
},
Spec: v1alpha1.ProjectSpec{
Repository: "org/repo",
},
},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

catalogIntf := k8s.TypedInterface[v1alpha1.Catalog](client, schema.GroupVersionResource{
Group: v1alpha1.CatalogGK.Group,
Version: "v1alpha1",
Resource: "catalogs",
})

_, err = catalogIntf.Apply(
t.Context(),
&v1alpha1.Catalog{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha1.GroupVersion.Identifier(),
Kind: v1alpha1.CatalogGK.Kind,
},
ObjectMeta: metav1.ObjectMeta{
Name: "catalog",
},
Spec: v1alpha1.CatalogSpec{
RepoURL: "https://github.com/testing/catalog",
Revision: "main",
Charts: v1alpha1.CatalogCharts{
Default: "default",
Refs: map[string]helm.Chart{
"default": {
Name: "alpha",
RepoURL: "file:///data/chart",
Version: "6.6.6",
Mappings: map[string]any{},
},
},
},
},
},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

envIntf := k8s.TypedInterface[v1alpha1.Environment](client, schema.GroupVersionResource{
Group: v1alpha1.EnvironmentGK.Group,
Version: "v1alpha1",
Resource: "environments",
})

env, err := envIntf.Apply(
t.Context(),
&v1alpha1.Environment{
ApiVersion: v1alpha1.GroupVersion.Identifier(),
Kind: v1alpha1.EnvironmentGK.Kind,
EnvironmentMetadata: v1alpha1.EnvironmentMetadata{ObjectMeta: metav1.ObjectMeta{Name: "staging"}},
Spec: v1alpha1.EnvironmentSpec{
Values: map[string]any{
"env": "alpha",
},
},
},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

EventuallyNoErrorf(
t,
func() error {
_, err := client.Clientset.CoreV1().Namespaces().Get(t.Context(), "staging", metav1.GetOptions{})
return err
},
50*time.Millisecond,
2*time.Second,
"failed to get corresponding namespace for env",
)

releaseIntf := k8s.TypedInterface[v1alpha1.Release](client, schema.GroupVersionResource{
Group: v1alpha1.ReleaseGK.Group,
Version: "v1alpha1",
Resource: "releases",
})

releaseIntf = releaseIntf.Namespace(env.Name)

release, err := releaseIntf.Apply(
t.Context(),
&v1alpha1.Release{
Kind: v1alpha1.ReleaseGK.Kind,
ApiVersion: v1alpha1.GroupVersion.Identifier(),
ReleaseMetadata: v1alpha1.ReleaseMetadata{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: env.Name,
Annotations: map[string]string{
"argocd.nesto.ca/sync.prune": "true",
},
},
},
Spec: v1alpha1.ReleaseSpec{
Project: project.Name,
Version: "1.2.3",
Namespace: "custom",
Values: map[string]any{
"freehand": "hello",
"env": "{{ .Environment.Spec.Values.env }}",
},
},
},
metav1.ApplyOptions{FieldManager: joyOperator},
)
require.NoError(t, err)

EventuallyNoErrorf(
t,
func() error {
release, err := releaseIntf.Get(t.Context(), release.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get release: %w", err)
}
if !slices.Contains(release.Finalizers, finalizerPruneRelease) {
return fmt.Errorf("release does not container prune-release finalizer")
}
return nil
},
time.Second,
5*time.Second,
"release never reached expected state",
)

appsIntf := k8s.TypedInterface[argocd.Application](client, argocd.ApplicationGVR).Namespace("argocd")

EventuallyNoErrorf(
t,
func() error {
_, err := appsIntf.Get(t.Context(), "staging-test", metav1.GetOptions{})
return err
},
50*time.Millisecond,
2*time.Second,
"failed to get staging-test application",
)

rel, err := releaseIntf.Get(t.Context(), release.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, []string{finalizerPruneRelease}, rel.Finalizers)
require.NoError(t, releaseIntf.Delete(t.Context(), release.Name, metav1.DeleteOptions{}))

_, err = releaseIntf.Get(t.Context(), release.Name, metav1.GetOptions{})
require.NoError(t, err)

EventuallyNoErrorf(
t,
func() error {
release, err := appsIntf.Get(t.Context(), "staging-test", metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
return nil
}
return fmt.Errorf("expected app to be not found but got: %v", err)
}
if release.DeletionTimestamp.IsZero() {
return fmt.Errorf("expected application to have a deletion timestamp but did not")
}
return nil
},
500*time.Millisecond,
10*time.Second,
"failed to see release application pruned",
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func EventuallyNoErrorf(t *testing.T, fn func() error, tick time.Duration, timeout time.Duration, msg string, args ...any) {
var (
ticker = time.NewTimer(0)
Expand Down
25 changes: 23 additions & 2 deletions cmd/operator/reconciler_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/util/retry"

"github.com/nestoca/joy/api/v1alpha1"

Expand Down Expand Up @@ -109,7 +110,9 @@ func EnvironmentReconciler(params EnvironmentReconcilerParams) ctrl.Funcs {
Namespace: ns.Name,
},
SyncPolicy: argocd.SyncPolicy{
Automated: &argocd.SyncPolicyAutomated{},
Automated: &argocd.SyncPolicyAutomated{
Prune: new(true),
},
},
},
},
Expand All @@ -118,7 +121,25 @@ func EnvironmentReconciler(params EnvironmentReconcilerParams) ctrl.Funcs {
return ctrl.Result{}, fmt.Errorf("failed to apply application: %w", err)
}
} else {
if err := appIntf.Delete(ctx, event.Name, metav1.DeleteOptions{}); err != nil && !kerrors.IsNotFound(err) {
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
app, err := appIntf.Get(ctx, event.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get application: %w", err)
}
app.Spec.SyncPolicy = argocd.SyncPolicy{
Automated: &argocd.SyncPolicyAutomated{
Prune: new(false),
},
}

app, err = appIntf.Apply(ctx, app, metav1.ApplyOptions{FieldManager: joyOperator, Force: true})
if err != nil {
return fmt.Errorf("failed to update syncPolicy to not prune: %w", err)
}
// We want a guarantee that when we delete the application that it was against a version of the resource who has prune de-activated.
// If we move from pull mode to non-pull mode, we don't want to drop all releases.
return appIntf.Delete(ctx, app.Name, metav1.DeleteOptions{Preconditions: &metav1.Preconditions{ResourceVersion: &app.ResourceVersion}})
}); err != nil && !kerrors.IsNotFound(err) {
return ctrl.Result{}, fmt.Errorf("failed to delete application: %w", err)
}
}
Expand Down
Loading