Skip to content

Commit 96ded48

Browse files
authored
feat: adds cleanup logic to operator (#165)
1 parent a5c815b commit 96ded48

File tree

11 files changed

+263
-46
lines changed

11 files changed

+263
-46
lines changed

foundry/operator/api/v1alpha1/release_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ type ReleaseDeploymentSpec struct {
2727

2828
// ReleaseID is the identifier for the release this deployment belongs to.
2929
ReleaseID string `json:"release_id"`
30+
31+
// TTL specifies the time to live for this deployment after completion (in seconds).
32+
// After this period has elapsed since completion, the operator will delete the resource.
33+
// +optional
34+
// +kubebuilder:default=300
35+
TTL int64 `json:"ttl,omitempty"`
3036
}
3137

3238
// GitSpec defines the source Git repository for the release.
@@ -49,6 +55,10 @@ type ReleaseDeploymentStatus struct {
4955

5056
// State is the current state of the release.
5157
State string `json:"state"`
58+
59+
// CompletionTime represents the time when this deployment completed (succeeded or failed).
60+
// +optional
61+
CompletionTime *metav1.Time `json:"completionTime,omitempty"`
5262
}
5363

5464
// +kubebuilder:object:root=true

foundry/operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

foundry/operator/cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ func main() {
239239
if err = (&controller.ReleaseDeploymentReconciler{
240240
Client: mgr.GetClient(),
241241
Config: cfg,
242-
DeploymentHandler: handlers.NewReleaseDeploymentHandler(context.Background(), apiClient),
242+
DeploymentHandler: handlers.NewReleaseDeploymentHandler(context.Background(), apiClient, mgr.GetClient()),
243243
Logger: logger,
244244
ManifestStore: deployment.NewDefaultManifestGeneratorStore(),
245245
Remote: remote.GoGitRemoteInteractor{},

foundry/operator/config/crd/bases/foundry.projectcatalyst.io_releasedeployments.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,25 @@ spec:
4646
description: ReleaseID is the identifier for the release this deployment
4747
belongs to.
4848
type: string
49+
ttl:
50+
default: 300
51+
description: |-
52+
TTL specifies the time to live for this deployment after completion (in seconds).
53+
After this period has elapsed since completion, the operator will delete the resource.
54+
format: int64
55+
type: integer
4956
required:
5057
- id
5158
- release_id
5259
type: object
5360
status:
5461
description: ReleaseDeploymentStatus defines the observed state of Release.
5562
properties:
63+
completionTime:
64+
description: CompletionTime represents the time when this deployment
65+
completed (succeeded or failed).
66+
format: date-time
67+
type: string
5668
conditions:
5769
items:
5870
description: Condition contains details for one aspect of the current

foundry/operator/config/rbac/role.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ rules:
77
- apiGroups:
88
- foundry.projectcatalyst.io
99
resources:
10-
- releasedeployments
10+
- releases
1111
verbs:
1212
- create
1313
- delete
@@ -19,13 +19,13 @@ rules:
1919
- apiGroups:
2020
- foundry.projectcatalyst.io
2121
resources:
22-
- releasedeployments/finalizers
22+
- releases/finalizers
2323
verbs:
2424
- update
2525
- apiGroups:
2626
- foundry.projectcatalyst.io
2727
resources:
28-
- releasedeployments/status
28+
- releases/status
2929
verbs:
3030
- get
3131
- patch

foundry/operator/internal/controller/release_controller.go

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919
import (
2020
"context"
2121
"log/slog"
22+
"time"
2223

2324
"cuelang.org/go/cue/cuecontext"
2425
"k8s.io/apimachinery/pkg/runtime"
@@ -48,17 +49,17 @@ type ReleaseDeploymentReconciler struct {
4849
SecretStore secrets.SecretStore
4950
}
5051

51-
// +kubebuilder:rbac:groups=foundry.projectcatalyst.io,resources=releases,verbs=get;list;watch;create;update;patch;delete
52-
// +kubebuilder:rbac:groups=foundry.projectcatalyst.io,resources=releases/status,verbs=get;update;patch
53-
// +kubebuilder:rbac:groups=foundry.projectcatalyst.io,resources=releases/finalizers,verbs=update
52+
// +kubebuilder:rbac:groups=foundry.projectcatalyst.io,resources=releasedeployments,verbs=get;list;watch;create;update;patch;delete
53+
// +kubebuilder:rbac:groups=foundry.projectcatalyst.io,resources=releasedeployments/status,verbs=get;update;patch
54+
// +kubebuilder:rbac:groups=foundry.projectcatalyst.io,resources=releasedeployments/finalizers,verbs=update
5455

5556
func (r *ReleaseDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
5657
log := log.FromContext(ctx)
5758

5859
// 1. Fetch the ReleaseDeployment resource
5960
var resource foundryv1alpha1.ReleaseDeployment
6061
if err := r.Get(ctx, req.NamespacedName, &resource); err != nil {
61-
log.Error(err, "unable to fetch Release")
62+
log.Error(err, "unable to fetch ReleaseDeployment")
6263
return ctrl.Result{}, client.IgnoreNotFound(err)
6364
}
6465

@@ -68,12 +69,39 @@ func (r *ReleaseDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Re
6869
log.Error(err, "unable to load deployment")
6970
return ctrl.Result{}, err
7071
}
71-
release := r.DeploymentHandler.Release()
7272

73-
// 3. Check if the deployment has already been completed
73+
// 3. Check for TTL expiration on completed deployments
7474
if r.DeploymentHandler.IsCompleted() {
75-
log.Info("Deployment already finished")
76-
return ctrl.Result{}, nil
75+
log.Info("Deployment is completed, checking TTL")
76+
77+
if err := r.DeploymentHandler.UpdateCompletionTime(); err != nil {
78+
log.Error(err, "unable to update completion time")
79+
return ctrl.Result{}, err
80+
}
81+
82+
expired, timeUntilExpiry := r.DeploymentHandler.IsExpired()
83+
if expired {
84+
log.Info("Deployment TTL has expired, deleting resource",
85+
"completionTime", resource.Status.CompletionTime,
86+
"ttl", resource.Spec.TTL)
87+
88+
if err := r.Delete(ctx, &resource); err != nil {
89+
log.Error(err, "unable to delete expired deployment")
90+
return ctrl.Result{}, err
91+
}
92+
93+
return ctrl.Result{}, nil
94+
}
95+
96+
// Add a 5 second buffer to avoid requeuing too early
97+
requeueAfter := timeUntilExpiry + 5*time.Second
98+
log.Info("Deployment not expired yet, requeuing",
99+
"completionTime", resource.Status.CompletionTime,
100+
"ttl", resource.Spec.TTL,
101+
"timeUntilExpiry", timeUntilExpiry.String(),
102+
"requeueAfter", requeueAfter.String())
103+
104+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
77105
}
78106

79107
// 4. Check if max attempts have been reached
@@ -94,19 +122,15 @@ func (r *ReleaseDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Re
94122
return ctrl.Result{}, err
95123
}
96124

97-
// resource.Status.State = "Deploying"
98-
// if err := r.Status().Update(ctx, &resource); err != nil {
99-
// log.Error(err, "unable to update Release status")
100-
// return ctrl.Result{}, err
101-
// }
102-
103125
// 6. Open repos
104126
log.Info("Opening deployment repo", "url", r.Config.Deployer.Git.Url)
105127
if err := r.RepoHandler.LoadDeploymentRepo(r.Config.Deployer.Git.Url, r.Config.Deployer.Git.Ref); err != nil {
106128
log.Error(err, "unable to load deployment repo")
129+
r.DeploymentHandler.AddErrorEvent(err, "Unable to load deployment repo")
107130
return ctrl.Result{}, err
108131
}
109132

133+
release := r.DeploymentHandler.Release()
110134
log.Info("Opening source repo", "url", release.SourceRepo)
111135
if err := r.RepoHandler.LoadSourceRepo(release.SourceRepo, release.SourceCommit); err != nil {
112136
log.Error(err, "unable to load source repo")
@@ -160,6 +184,7 @@ func (r *ReleaseDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Re
160184
return ctrl.Result{}, err
161185
}
162186

187+
// Requeue to check TTL later
163188
return ctrl.Result{}, nil
164189
}
165190

foundry/operator/internal/controller/release_controller_test.go

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
var _ = Describe("ReleaseDeployment Controller", func() {
3434
Context("When reconciling a release deployment", func() {
3535
const (
36-
timeout = time.Second * 10
36+
timeout = time.Second * 5
3737
interval = time.Millisecond * 250
3838
)
3939

@@ -43,46 +43,40 @@ var _ = Describe("ReleaseDeployment Controller", func() {
4343
)
4444

4545
BeforeAll(func() {
46-
// Initialize the test environment
4746
env.Init(
4847
map[string]string{
4948
"project/blueprint.cue": newRawBlueprint(),
5049
},
5150
map[string]string{
5251
"root/test/project/env.cue": `main: values: { key1: "value1" }`,
5352
},
53+
k8sClient,
5454
)
5555
env.ConfigureController(controller)
5656

57-
release := &foundryv1alpha1.ReleaseDeployment{}
58-
err := k8sClient.Get(ctx, getNamespacedName(env.releaseDeploymentObj), release)
57+
err := k8sClient.Get(ctx, getNamespacedName(env.releaseDeploymentObj), env.releaseDeploymentObj)
5958
if err != nil && errors.IsNotFound(err) {
6059
Expect(k8sClient.Create(ctx, env.releaseDeploymentObj)).To(Succeed())
6160
}
6261
})
6362

64-
// AfterEach(func() {
65-
// // TODO(user): Cleanup logic after each test, like removing the resource instance.
66-
// resource := &foundryv1alpha1.Release{}
67-
// err := k8sClient.Get(ctx, typeNamespacedName, resource)
68-
// Expect(err).NotTo(HaveOccurred())
69-
70-
// By("Cleanup the specific resource instance Release")
71-
// Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
72-
// })
63+
AfterAll(func() {
64+
err := k8sClient.Get(ctx, getNamespacedName(env.releaseDeploymentObj), env.releaseDeploymentObj)
65+
if err == nil {
66+
Expect(k8sClient.Delete(ctx, env.releaseDeploymentObj)).To(Succeed())
67+
}
68+
})
7369

7470
It("should set the status to succeeded", func() {
75-
//release := &foundryv1alpha1.ReleaseDeployment{}
7671
Eventually(func(g Gomega) {
77-
//g.Expect(k8sClient.Get(ctx, getNamespacedName(releaseDeploymentObj), release)).To(Succeed())
78-
//g.Expect(release.Status.State).To(Equal("Deployed"))
72+
g.Expect(k8sClient.Get(ctx, getNamespacedName(env.releaseDeploymentObj), env.releaseDeploymentObj)).To(Succeed())
73+
g.Expect(env.releaseDeploymentObj.Status.State).To(Equal(string(api.DeploymentStatusSucceeded)))
7974
g.Expect(env.releaseDeployment.Status).To(Equal(api.DeploymentStatusSucceeded))
8075
g.Expect(hasEvent(env.releaseDeployment.Events, "DeploymentSucceeded", "Deployment has succeeded")).To(BeTrue())
8176
}, timeout, interval).Should(Succeed())
8277
})
8378

8479
It("should have called the API client to get the deployment", func() {
85-
Expect(len(env.mockClient.GetDeploymentCalls())).To(Equal(1))
8680
Expect(env.mockClient.GetDeploymentCalls()[0].DeployID).To(Equal(env.releaseDeployment.ID))
8781
Expect(env.mockClient.GetDeploymentCalls()[0].ReleaseID).To(Equal(env.releaseDeployment.Release.ID))
8882
})
@@ -124,6 +118,26 @@ var _ = Describe("ReleaseDeployment Controller", func() {
124118
Expect(err).NotTo(HaveOccurred())
125119
Expect(string(got)).To(Equal(string(env.manifestContent)))
126120
})
121+
122+
Context("when the ttl expires", func() {
123+
BeforeAll(func() {
124+
err := k8sClient.Get(ctx, getNamespacedName(env.releaseDeploymentObj), env.releaseDeploymentObj)
125+
Expect(err).To(Succeed())
126+
127+
env.mockClock.Advance(time.Hour * 1)
128+
env.releaseDeploymentObj.Spec.TTL = 1
129+
Expect(k8sClient.Update(ctx, env.releaseDeploymentObj)).To(Succeed())
130+
})
131+
132+
It("should delete the deployment", func() {
133+
Eventually(func(g Gomega) {
134+
release := &foundryv1alpha1.ReleaseDeployment{}
135+
err := k8sClient.Get(ctx, getNamespacedName(env.releaseDeploymentObj), release)
136+
g.Expect(err).To(HaveOccurred())
137+
g.Expect(errors.IsNotFound(err)).To(BeTrue())
138+
}, timeout, interval).Should(Succeed())
139+
})
140+
})
127141
})
128142
})
129143
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package controller
2+
3+
import "time"
4+
5+
// MockClock is a Clock that can be controlled for testing.
6+
type MockClock struct {
7+
CurrentTime time.Time
8+
}
9+
10+
func (m *MockClock) Now() time.Time {
11+
return m.CurrentTime
12+
}
13+
14+
func (m *MockClock) Since(t time.Time) time.Duration {
15+
return m.CurrentTime.Sub(t)
16+
}
17+
18+
func (m *MockClock) Until(t time.Time) time.Duration {
19+
return t.Sub(m.CurrentTime)
20+
}
21+
22+
func (m *MockClock) SetTime(t time.Time) {
23+
m.CurrentTime = t
24+
}
25+
26+
func (m *MockClock) Advance(d time.Duration) {
27+
m.CurrentTime = m.CurrentTime.Add(d)
28+
}
29+
30+
func NewMockClock(initialTime time.Time) *MockClock {
31+
return &MockClock{CurrentTime: initialTime}
32+
}

foundry/operator/internal/controller/suite_env_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"path/filepath"
7+
"time"
78

89
"cuelang.org/go/cue/cuecontext"
910
"github.com/go-git/go-billy/v5"
@@ -29,6 +30,7 @@ import (
2930
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
3031
. "github.com/onsi/gomega"
3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
3234
)
3335

3436
type mockEnv struct {
@@ -39,6 +41,7 @@ type mockEnv struct {
3941
deployFs *bfs.BillyFs
4042
manifestContent string
4143
mockClient *am.ClientMock
44+
mockClock *MockClock
4245
mockDeploymentHandler *handlers.ReleaseDeploymentHandler
4346
mockManifestStore deployment.ManifestGeneratorStore
4447
mockRemote *rm.GitRemoteInteractorMock
@@ -50,7 +53,7 @@ type mockEnv struct {
5053
sourceRepo *gg.Repository
5154
}
5255

53-
func (m *mockEnv) Init(sourceFiles, deployFiles map[string]string) {
56+
func (m *mockEnv) Init(sourceFiles, deployFiles map[string]string, k8sClient k8sclient.Client) {
5457
var err error
5558

5659
// Setup filesystems
@@ -122,7 +125,8 @@ func (m *mockEnv) Init(sourceFiles, deployFiles map[string]string) {
122125
m.mockSecretStore = tu.NewMockSecretStore(map[string]string{"token": "value"})
123126

124127
// Setup the mock handlers
125-
m.mockDeploymentHandler = handlers.NewReleaseDeploymentHandler(context.Background(), m.mockClient)
128+
m.mockClock = NewMockClock(time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC))
129+
m.mockDeploymentHandler = handlers.NewReleaseDeploymentHandlerWithClock(context.Background(), m.mockClient, k8sClient, m.mockClock)
126130
m.mockRepoHandler = handlers.NewRepoHandler(m.deployFs, m.sourceFs, testutils.NewNoopLogger(), m.mockRemote, "token")
127131

128132
// Initialize the source repository (to properly set the source commit)

0 commit comments

Comments
 (0)