Skip to content

Commit 279f207

Browse files
committed
Wait for Artifact before generation.
The artifact may or may not be available, if we've already generated resources, generating no resources means that the existing resources are removed. This changes it to return a marker error, which we can record and do nothing about. We can do nothing, because when the Artifact is applied, this will trigger a watch notification, and we can generate.
1 parent aa81746 commit 279f207

File tree

9 files changed

+138
-62
lines changed

9 files changed

+138
-62
lines changed

controllers/gitopsset_controller.go

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controllers
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"sort"
78
"time"
@@ -164,6 +165,16 @@ func (r *GitOpsSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
164165
inventory, requeue, err := r.reconcileResources(ctx, k8sClient, &gitOpsSet)
165166

166167
if err != nil {
168+
// We can return here because when the resource artifact is updated, this
169+
// will trigger a reconciliation.
170+
if errors.As(err, &generators.NoArtifactError{}) {
171+
templatesv1.SetGitOpsSetReadiness(&gitOpsSet, metav1.ConditionFalse, templatesv1.ReconciliationFailedReason, "waiting for artifact")
172+
if err := r.patchStatus(ctx, req, gitOpsSet.Status); err != nil {
173+
logger.Error(err, "failed to reconcile")
174+
}
175+
return ctrl.Result{}, nil
176+
}
177+
167178
templatesv1.SetGitOpsSetReadiness(&gitOpsSet, metav1.ConditionFalse, templatesv1.ReconciliationFailedReason, err.Error())
168179
if err := r.patchStatus(ctx, req, gitOpsSet.Status); err != nil {
169180
logger.Error(err, "failed to reconcile")

controllers/gitopsset_controller_test.go

+42-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
1212
"github.com/fluxcd/pkg/apis/meta"
1313
fluxMeta "github.com/fluxcd/pkg/apis/meta"
14+
sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
1415
"github.com/google/go-cmp/cmp"
1516
"github.com/google/go-cmp/cmp/cmpopts"
1617
corev1 "k8s.io/api/core/v1"
@@ -32,6 +33,7 @@ import (
3233
clustersv1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
3334
templatesv1 "github.com/weaveworks/gitopssets-controller/api/v1alpha1"
3435
"github.com/weaveworks/gitopssets-controller/controllers/templates/generators"
36+
"github.com/weaveworks/gitopssets-controller/controllers/templates/generators/gitrepository"
3537
"github.com/weaveworks/gitopssets-controller/controllers/templates/generators/list"
3638
"github.com/weaveworks/gitopssets-controller/test"
3739
)
@@ -54,6 +56,7 @@ func TestReconciliation(t *testing.T) {
5456
CRDDirectoryPaths: []string{
5557
filepath.Join("..", "config", "crd", "bases"),
5658
"testdata/crds",
59+
"../tests/e2e/testdata/crds",
5760
},
5861
}
5962
testEnv.ControlPlane.GetAPIServer().Configure().Append("--authorization-mode=RBAC")
@@ -73,6 +76,7 @@ func TestReconciliation(t *testing.T) {
7376
// Kustomizations.
7477
test.AssertNoError(t, clientgoscheme.AddToScheme(scheme))
7578
test.AssertNoError(t, templatesv1.AddToScheme(scheme))
79+
test.AssertNoError(t, sourcev1beta2.AddToScheme(scheme))
7680

7781
k8sClient, err := client.New(cfg, client.Options{Scheme: scheme})
7882
test.AssertNoError(t, err)
@@ -86,7 +90,8 @@ func TestReconciliation(t *testing.T) {
8690
Scheme: scheme,
8791
DefaultServiceAccount: "",
8892
Generators: map[string]generators.GeneratorFactory{
89-
"List": list.GeneratorFactory,
93+
"List": list.GeneratorFactory,
94+
"GitRepository": gitrepository.GeneratorFactory(nil),
9095
},
9196
Config: cfg,
9297
EventRecorder: eventRecorder,
@@ -200,7 +205,7 @@ func TestReconciliation(t *testing.T) {
200205
}
201206
})
202207
test.AssertNoError(t, k8sClient.Create(ctx, test.ToUnstructured(t, devKS)))
203-
defer cleanupResource(t, k8sClient, test.ToUnstructured(t, devKS))
208+
defer deleteObject(t, k8sClient, test.ToUnstructured(t, devKS))
204209

205210
_, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(gs)})
206211
test.AssertErrorMatch(t, "failed to create Resource.*already exists", err)
@@ -731,6 +736,37 @@ func TestReconciliation(t *testing.T) {
731736
assertNoKustomizationsExistInNamespace(t, k8sClient, "default")
732737
})
733738

739+
t.Run("reconciling when gitrepository has no artifact", func(t *testing.T) {
740+
ctx := context.TODO()
741+
emptyGR := test.NewGitRepository()
742+
test.AssertNoError(t, k8sClient.Create(ctx, test.ToUnstructured(t, emptyGR)))
743+
defer deleteObject(t, k8sClient, emptyGR)
744+
745+
gs := createAndReconcileToFinalizedState(t, k8sClient, reconciler, makeTestGitOpsSet(t, func(gs *templatesv1.GitOpsSet) {
746+
gs.Spec.Generators = []templatesv1.GitOpsSetGenerator{
747+
{
748+
GitRepository: &templatesv1.GitRepositoryGenerator{
749+
RepositoryRef: "test-repository",
750+
Files: []templatesv1.RepositoryGeneratorFileItem{
751+
{Path: "files/dev.yaml"},
752+
{Path: "files/production.yaml"},
753+
{Path: "files/staging.yaml"},
754+
},
755+
},
756+
},
757+
}
758+
}))
759+
defer deleteGitOpsSetAndFinalize(t, k8sClient, reconciler, gs)
760+
761+
_, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(gs)})
762+
test.AssertNoError(t, err)
763+
764+
test.AssertNoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(gs), gs))
765+
assertInventoryHasNoItems(t, gs)
766+
assertNoKustomizationsExistInNamespace(t, k8sClient, "default")
767+
assertGitOpsSetCondition(t, gs, meta.ReadyCondition, "waiting for artifact")
768+
})
769+
734770
t.Run("error conditions - existing resource", func(t *testing.T) {
735771
ctx := context.TODO()
736772
gs := makeTestGitOpsSet(t, func(gs *templatesv1.GitOpsSet) {
@@ -760,7 +796,7 @@ func TestReconciliation(t *testing.T) {
760796
}
761797
})
762798
test.AssertNoError(t, k8sClient.Create(ctx, test.ToUnstructured(t, devKS)))
763-
defer cleanupResource(t, k8sClient, gs)
799+
defer deleteObject(t, k8sClient, gs)
764800

765801
_, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(gs)})
766802
test.AssertErrorMatch(t, "failed to create Resource.*already exists", err)
@@ -1098,7 +1134,7 @@ func assertInventoryHasNoItems(t *testing.T, gs *templatesv1.GitOpsSet) {
10981134
}
10991135
}
11001136

1101-
func cleanupResource(t *testing.T, cl client.Client, obj client.Object) {
1137+
func deleteObject(t *testing.T, cl client.Client, obj client.Object) {
11021138
t.Helper()
11031139
if err := cl.Delete(context.TODO(), obj); err != nil {
11041140
t.Fatal(err)
@@ -1226,7 +1262,7 @@ func createRBACForServiceAccount(t *testing.T, cl client.Client, serviceAccountN
12261262
t.Fatalf("failed to write role: %s", err)
12271263
}
12281264
t.Cleanup(func() {
1229-
cleanupResource(t, cl, role)
1265+
deleteObject(t, cl, role)
12301266
})
12311267
binding := &rbacv1.RoleBinding{
12321268
ObjectMeta: metav1.ObjectMeta{
@@ -1248,7 +1284,7 @@ func createRBACForServiceAccount(t *testing.T, cl client.Client, serviceAccountN
12481284
t.Fatalf("failed to write role-binding: %s", err)
12491285
}
12501286
t.Cleanup(func() {
1251-
cleanupResource(t, cl, binding)
1287+
deleteObject(t, cl, binding)
12521288
})
12531289
}
12541290
func deleteGitOpsSetAndFinalize(t *testing.T, cl client.Client, reconciler *GitOpsSetReconciler, gs *templatesv1.GitOpsSet) {
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package generators
2+
3+
import (
4+
"fmt"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
)
8+
9+
// NoArtifactError indicates that a Repository's artifact is not available.
10+
type NoArtifactError struct {
11+
Kind string
12+
Name client.ObjectKey
13+
}
14+
15+
func (e NoArtifactError) Error() string {
16+
return fmt.Sprintf("no artifact for %s %s", e.Kind, e.Name)
17+
}
18+
19+
// ArtifactError creates and returns a new Artifact error.
20+
func ArtifactError(kind string, name client.ObjectKey) error {
21+
return NoArtifactError{
22+
Kind: kind,
23+
Name: name,
24+
}
25+
}

controllers/templates/generators/gitrepository/git_repository.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (g *GitRepositoryGenerator) generateParamsFromGitFiles(ctx context.Context,
7373
// No artifact? nothing to generate...
7474
if gr.Status.Artifact == nil {
7575
g.Logger.Info("GitRepository does not have an artifact", "repository", repoName)
76-
return []map[string]any{}, nil
76+
return nil, generators.ArtifactError("GitRepository", repoName)
7777
}
7878

7979
g.Logger.Info("fetching archive URL", "repoURL", gr.Spec.URL, "artifactURL", gr.Status.Artifact.URL,

controllers/templates/generators/gitrepository/git_repository_test.go

+17-34
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ const testRetries int = 3
2424

2525
var _ generators.Generator = (*GitRepositoryGenerator)(nil)
2626

27-
const testNamespace = "generation"
28-
2927
var testFetcher = fetch.NewArchiveFetcher(testRetries, tar.UnlimitedUntarSize, tar.UnlimitedUntarSize, "")
3028

3129
func TestGenerate_with_no_GitRepository(t *testing.T) {
@@ -48,19 +46,6 @@ func TestGenerate(t *testing.T) {
4846
objects []runtime.Object
4947
want []map[string]any
5048
}{
51-
{
52-
"no artifact in GitRepository",
53-
&templatesv1.GitRepositoryGenerator{
54-
RepositoryRef: "test-repository",
55-
Files: []templatesv1.RepositoryGeneratorFileItem{
56-
{Path: "files/dev.yaml"},
57-
{Path: "files/production.yaml"},
58-
{Path: "files/staging.yaml"},
59-
},
60-
},
61-
[]runtime.Object{newGitRepository()},
62-
[]map[string]any{},
63-
},
6449
{
6550
"file list case",
6651
&templatesv1.GitRepositoryGenerator{
@@ -71,7 +56,7 @@ func TestGenerate(t *testing.T) {
7156
{Path: "files/staging.yaml"},
7257
},
7358
},
74-
[]runtime.Object{newGitRepository(
59+
[]runtime.Object{test.NewGitRepository(
7560
withArchiveURLAndChecksum(srv.URL+"/files.tar.gz",
7661
"sha256:f0a57ec1cdebda91cf00d89dfa298c6ac27791e7fdb0329990478061755eaca8"))},
7762
[]map[string]any{
@@ -88,7 +73,7 @@ func TestGenerate(t *testing.T) {
8873
{Path: "applications/*"},
8974
},
9075
},
91-
[]runtime.Object{newGitRepository(
76+
[]runtime.Object{test.NewGitRepository(
9277
withArchiveURLAndChecksum(srv.URL+"/directories.tar.gz",
9378
"sha256:a8bb41d733c5cc9bdd13d926a2edbe4c85d493c6c90271da1e1b991880935dc1"))},
9479
[]map[string]any{
@@ -107,7 +92,7 @@ func TestGenerate(t *testing.T) {
10792
&templatesv1.GitOpsSet{
10893
ObjectMeta: metav1.ObjectMeta{
10994
Name: "test-generator",
110-
Namespace: testNamespace,
95+
Namespace: "default",
11196
},
11297
Spec: templatesv1.GitOpsSetSpec{
11398
Generators: []templatesv1.GitOpsSetGenerator{
@@ -163,6 +148,19 @@ func TestGenerate_errors(t *testing.T) {
163148
},
164149
wantErr: "GitOpsSet is empty",
165150
},
151+
{
152+
name: "no artifact in GitRepository",
153+
generator: &templatesv1.GitRepositoryGenerator{
154+
RepositoryRef: "test-repository",
155+
Files: []templatesv1.RepositoryGeneratorFileItem{
156+
{Path: "files/dev.yaml"},
157+
{Path: "files/production.yaml"},
158+
{Path: "files/staging.yaml"},
159+
},
160+
},
161+
objects: []runtime.Object{test.NewGitRepository()},
162+
wantErr: "no artifact for GitRepository default/test-repository",
163+
},
166164
}
167165

168166
for _, tt := range testCases {
@@ -174,7 +172,7 @@ func TestGenerate_errors(t *testing.T) {
174172
&templatesv1.GitOpsSet{
175173
ObjectMeta: metav1.ObjectMeta{
176174
Name: "test-generator",
177-
Namespace: testNamespace,
175+
Namespace: "default",
178176
},
179177
Spec: templatesv1.GitOpsSetSpec{
180178
Generators: []templatesv1.GitOpsSetGenerator{
@@ -199,21 +197,6 @@ func withArchiveURLAndChecksum(archiveURL, xsum string) func(*sourcev1beta2.GitR
199197
}
200198
}
201199

202-
func newGitRepository(opts ...func(*sourcev1beta2.GitRepository)) *sourcev1beta2.GitRepository {
203-
gr := &sourcev1beta2.GitRepository{
204-
ObjectMeta: metav1.ObjectMeta{
205-
Name: "test-repository",
206-
Namespace: testNamespace,
207-
},
208-
}
209-
210-
for _, opt := range opts {
211-
opt(gr)
212-
}
213-
214-
return gr
215-
}
216-
217200
func newFakeClient(t *testing.T, objs ...runtime.Object) client.WithWatch {
218201
t.Helper()
219202
scheme := runtime.NewScheme()

controllers/templates/generators/interface.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ type Generator interface {
3131
// ErrEmptyGitOpsSetGenerator is returned when GitOpsSet is
3232
// empty.
3333
var ErrEmptyGitOpsSet = errors.New("GitOpsSet is empty")
34-
var NoRequeueInterval time.Duration
3534

36-
var ErrIncorrectNumberOfGenerators = errors.New("matrix generator needs two generators")
35+
var NoRequeueInterval time.Duration
3736

3837
// DefaultInterval is used when Interval is not specified, it
3938
// is the default time to wait before the next reconcile loop.

controllers/templates/generators/ocirepository/oci_repository.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (g *OCIRepositoryGenerator) generateParamsFromOCIFiles(ctx context.Context,
7373
// No artifact? nothing to generate...
7474
if gr.Status.Artifact == nil {
7575
g.Logger.Info("OCIRepository does not have an artifact", "repository", repoName)
76-
return []map[string]any{}, nil
76+
return nil, generators.ArtifactError("OCIRepository", repoName)
7777
}
7878

7979
g.Logger.Info("fetching archive URL", "repoURL", gr.Spec.URL, "artifactURL", gr.Status.Artifact.URL,

controllers/templates/generators/ocirepository/oci_repository_test.go

+16-18
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ const testRetries int = 3
2424

2525
var _ generators.Generator = (*OCIRepositoryGenerator)(nil)
2626

27-
const testNamespace = "generation"
28-
2927
var testFetcher = fetch.NewArchiveFetcher(testRetries, tar.UnlimitedUntarSize, tar.UnlimitedUntarSize, "")
3028

3129
func TestGenerate_with_no_OCIRepository(t *testing.T) {
@@ -48,19 +46,6 @@ func TestGenerate(t *testing.T) {
4846
objects []runtime.Object
4947
want []map[string]any
5048
}{
51-
{
52-
"no artifact in OCIRepository",
53-
&templatesv1.OCIRepositoryGenerator{
54-
RepositoryRef: "test-repository",
55-
Files: []templatesv1.RepositoryGeneratorFileItem{
56-
{Path: "files/dev.yaml"},
57-
{Path: "files/production.yaml"},
58-
{Path: "files/staging.yaml"},
59-
},
60-
},
61-
[]runtime.Object{newOCIRepository()},
62-
[]map[string]any{},
63-
},
6449
{
6550
"file list case",
6651
&templatesv1.OCIRepositoryGenerator{
@@ -107,7 +92,7 @@ func TestGenerate(t *testing.T) {
10792
&templatesv1.GitOpsSet{
10893
ObjectMeta: metav1.ObjectMeta{
10994
Name: "test-generator",
110-
Namespace: testNamespace,
95+
Namespace: "default",
11196
},
11297
Spec: templatesv1.GitOpsSetSpec{
11398
Generators: []templatesv1.GitOpsSetGenerator{
@@ -163,6 +148,19 @@ func TestGenerate_errors(t *testing.T) {
163148
},
164149
wantErr: "GitOpsSet is empty",
165150
},
151+
{
152+
name: "no artifact in OCIRepository",
153+
generator: &templatesv1.OCIRepositoryGenerator{
154+
RepositoryRef: "test-repository",
155+
Files: []templatesv1.RepositoryGeneratorFileItem{
156+
{Path: "files/dev.yaml"},
157+
{Path: "files/production.yaml"},
158+
{Path: "files/staging.yaml"},
159+
},
160+
},
161+
objects: []runtime.Object{newOCIRepository()},
162+
wantErr: "no artifact for OCIRepository default/test-repository",
163+
},
166164
}
167165

168166
for _, tt := range testCases {
@@ -174,7 +172,7 @@ func TestGenerate_errors(t *testing.T) {
174172
&templatesv1.GitOpsSet{
175173
ObjectMeta: metav1.ObjectMeta{
176174
Name: "test-generator",
177-
Namespace: testNamespace,
175+
Namespace: "default",
178176
},
179177
Spec: templatesv1.GitOpsSetSpec{
180178
Generators: []templatesv1.GitOpsSetGenerator{
@@ -203,7 +201,7 @@ func newOCIRepository(opts ...func(*sourcev1beta2.OCIRepository)) *sourcev1beta2
203201
gr := &sourcev1beta2.OCIRepository{
204202
ObjectMeta: metav1.ObjectMeta{
205203
Name: "test-repository",
206-
Namespace: testNamespace,
204+
Namespace: "default",
207205
},
208206
}
209207

0 commit comments

Comments
 (0)