Skip to content
This repository was archived by the owner on May 9, 2025. It is now read-only.

Commit 3f7a781

Browse files
committed
feat(ref): add conditions to git-sync-controller
1 parent 0074dc5 commit 3f7a781

File tree

7 files changed

+231
-36
lines changed

7 files changed

+231
-36
lines changed

api/v1alpha1/condition_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2022.
2+
// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors.
3+
//
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package v1alpha1
7+
8+
const (
9+
// PatchFailedReason is used when we couldn't patch an object.
10+
PatchFailedReason = "PatchFailed"
11+
12+
// SnapshotGetFailedReason is used when the needed snapshot does not exist.
13+
SnapshotGetFailedReason = "SnapshotGetFailed"
14+
15+
// AuthenticateGetFailedReason is used when the needed authentication does not exist.
16+
AuthenticateGetFailedReason = "AuthenticateGetFailed"
17+
18+
// GitRepositoryPushFailedReason is used when the needed pushing to a git repository failed.
19+
GitRepositoryPushFailedReason = "GitRepositoryPushFailed"
20+
)

api/v1alpha1/gitsync_types.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package v1alpha1
77

88
import (
9+
"time"
10+
911
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1012
)
1113

@@ -38,6 +40,30 @@ type GitSyncSpec struct {
3840
// GitSyncStatus defines the observed state of GitSync
3941
type GitSyncStatus struct {
4042
Digest string `json:"digest,omitempty"`
43+
44+
// ObservedGeneration is the last reconciled generation.
45+
// +optional
46+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
47+
// +optional
48+
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
49+
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
50+
Conditions []metav1.Condition `json:"conditions,omitempty"`
51+
}
52+
53+
// GetConditions returns the conditions of the ComponentVersion.
54+
func (in *GitSync) GetConditions() []metav1.Condition {
55+
return in.Status.Conditions
56+
}
57+
58+
// SetConditions sets the conditions of the ComponentVersion.
59+
func (in *GitSync) SetConditions(conditions []metav1.Condition) {
60+
in.Status.Conditions = conditions
61+
}
62+
63+
// GetRequeueAfter returns the duration after which the ComponentVersion must be
64+
// reconciled again.
65+
func (in GitSync) GetRequeueAfter() time.Duration {
66+
return in.Spec.Interval.Duration
4167
}
4268

4369
//+kubebuilder:object:root=true

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/delivery.ocm.software_gitsyncs.yaml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,80 @@ spec:
106106
status:
107107
description: GitSyncStatus defines the observed state of GitSync
108108
properties:
109+
conditions:
110+
items:
111+
description: "Condition contains details for one aspect of the current
112+
state of this API Resource. --- This struct is intended for direct
113+
use as an array at the field path .status.conditions. For example,
114+
\n type FooStatus struct{ // Represents the observations of a
115+
foo's current state. // Known .status.conditions.type are: \"Available\",
116+
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
117+
// +listType=map // +listMapKey=type Conditions []metav1.Condition
118+
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
119+
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
120+
properties:
121+
lastTransitionTime:
122+
description: lastTransitionTime is the last time the condition
123+
transitioned from one status to another. This should be when
124+
the underlying condition changed. If that is not known, then
125+
using the time when the API field changed is acceptable.
126+
format: date-time
127+
type: string
128+
message:
129+
description: message is a human readable message indicating
130+
details about the transition. This may be an empty string.
131+
maxLength: 32768
132+
type: string
133+
observedGeneration:
134+
description: observedGeneration represents the .metadata.generation
135+
that the condition was set based upon. For instance, if .metadata.generation
136+
is currently 12, but the .status.conditions[x].observedGeneration
137+
is 9, the condition is out of date with respect to the current
138+
state of the instance.
139+
format: int64
140+
minimum: 0
141+
type: integer
142+
reason:
143+
description: reason contains a programmatic identifier indicating
144+
the reason for the condition's last transition. Producers
145+
of specific condition types may define expected values and
146+
meanings for this field, and whether the values are considered
147+
a guaranteed API. The value should be a CamelCase string.
148+
This field may not be empty.
149+
maxLength: 1024
150+
minLength: 1
151+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
152+
type: string
153+
status:
154+
description: status of the condition, one of True, False, Unknown.
155+
enum:
156+
- "True"
157+
- "False"
158+
- Unknown
159+
type: string
160+
type:
161+
description: type of condition in CamelCase or in foo.example.com/CamelCase.
162+
--- Many .condition.type values are consistent across resources
163+
like Available, but because arbitrary conditions can be useful
164+
(see .node.status.conditions), the ability to deconflict is
165+
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
166+
maxLength: 316
167+
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
168+
type: string
169+
required:
170+
- lastTransitionTime
171+
- message
172+
- reason
173+
- status
174+
- type
175+
type: object
176+
type: array
109177
digest:
110178
type: string
179+
observedGeneration:
180+
description: ObservedGeneration is the last reconciled generation.
181+
format: int64
182+
type: integer
111183
type: object
112184
type: object
113185
served: true

controllers/gitsync_controller.go

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ package controllers
77

88
import (
99
"context"
10+
"errors"
1011
"fmt"
11-
"time"
1212

13+
"github.com/fluxcd/pkg/apis/meta"
14+
"github.com/fluxcd/pkg/runtime/conditions"
1315
"github.com/fluxcd/pkg/runtime/patch"
1416
corev1 "k8s.io/api/core/v1"
1517
apierrors "k8s.io/apimachinery/pkg/api/errors"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1619
"k8s.io/apimachinery/pkg/runtime"
1720
"k8s.io/apimachinery/pkg/types"
1821
ctrl "sigs.k8s.io/controller-runtime"
@@ -44,71 +47,134 @@ type GitSyncReconciler struct {
4447
// Reconcile is part of the main kubernetes reconciliation loop which aims to
4548
// move the current state of the cluster closer to the desired state.
4649
func (r *GitSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
50+
var (
51+
result ctrl.Result
52+
retErr error
53+
)
54+
4755
log := log.FromContext(ctx)
4856
log.V(4).Info("starting reconcile loop for snapshot")
49-
gitSync := &v1alpha1.GitSync{}
50-
if err := r.Get(ctx, req.NamespacedName, gitSync); err != nil {
57+
obj := &v1alpha1.GitSync{}
58+
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
5159
if apierrors.IsNotFound(err) {
5260
return ctrl.Result{}, nil
5361
}
5462
return ctrl.Result{}, fmt.Errorf("failed to get git sync object: %w", err)
5563
}
56-
log.V(4).Info("found reconciling object", "gitSync", gitSync)
64+
log.V(4).Info("found reconciling object", "gitSync", obj)
5765

58-
if gitSync.Status.Digest != "" {
59-
log.Info("GitSync object already synced; status contains digest information", "digest", gitSync.Status.Digest)
66+
if obj.Status.Digest != "" {
67+
log.Info("GitSync object already synced; status contains digest information", "digest", obj.Status.Digest)
6068
return ctrl.Result{}, nil
6169
}
6270

71+
// The replication controller doesn't need a shouldReconcile, because it should always reconcile,
72+
// that is its purpose.
73+
patchHelper, err := patch.NewHelper(obj, r.Client)
74+
if err != nil {
75+
retErr = errors.Join(retErr, err)
76+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.PatchFailedReason, err.Error())
77+
78+
return ctrl.Result{}, retErr
79+
}
80+
81+
// Always attempt to patch the object and status after each reconciliation.
82+
defer func() {
83+
// Patching has not been set up, or the controller errored earlier.
84+
if patchHelper == nil {
85+
return
86+
}
87+
88+
if condition := conditions.Get(obj, meta.StalledCondition); condition != nil && condition.Status == metav1.ConditionTrue {
89+
conditions.Delete(obj, meta.ReconcilingCondition)
90+
}
91+
92+
// Check if it's a successful reconciliation.
93+
// We don't set Requeue in case of error, so we can safely check for Requeue.
94+
if result.RequeueAfter == obj.GetRequeueAfter() && !result.Requeue && retErr == nil {
95+
// Remove the reconciling condition if it's set.
96+
conditions.Delete(obj, meta.ReconcilingCondition)
97+
98+
// Set the return err as the ready failure message if the resource is not ready, but also not reconciling or stalled.
99+
if ready := conditions.Get(obj, meta.ReadyCondition); ready != nil && ready.Status == metav1.ConditionFalse && !conditions.IsStalled(obj) {
100+
retErr = errors.New(conditions.GetMessage(obj, meta.ReadyCondition))
101+
}
102+
}
103+
104+
// If still reconciling then reconciliation did not succeed, set to ProgressingWithRetry to
105+
// indicate that reconciliation will be retried.
106+
if conditions.IsReconciling(obj) {
107+
reconciling := conditions.Get(obj, meta.ReconcilingCondition)
108+
reconciling.Reason = meta.ProgressingWithRetryReason
109+
conditions.Set(obj, reconciling)
110+
}
111+
112+
// If not reconciling or stalled than mark Ready=True
113+
if !conditions.IsReconciling(obj) &&
114+
!conditions.IsStalled(obj) &&
115+
retErr == nil &&
116+
result.RequeueAfter == obj.GetRequeueAfter() {
117+
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "Reconciliation success")
118+
}
119+
120+
// Set status observed generation option if the component is stalled or ready.
121+
if conditions.IsStalled(obj) || conditions.IsReady(obj) {
122+
obj.Status.ObservedGeneration = obj.Generation
123+
}
124+
125+
// Update the object.
126+
if err := patchHelper.Patch(ctx, obj); err != nil {
127+
retErr = errors.Join(retErr, err)
128+
}
129+
}()
130+
63131
snapshot := &ocmv1.Snapshot{}
64132
if err := r.Get(ctx, types.NamespacedName{
65-
Namespace: gitSync.Spec.SnapshotRef.Namespace,
66-
Name: gitSync.Spec.SnapshotRef.Name,
133+
Namespace: obj.Spec.SnapshotRef.Namespace,
134+
Name: obj.Spec.SnapshotRef.Name,
67135
}, snapshot); err != nil {
68-
return ctrl.Result{}, fmt.Errorf("failed to find snapshot: %w", err)
136+
retErr = fmt.Errorf("failed to find snapshot: %w", err)
137+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.SnapshotGetFailedReason, retErr.Error())
138+
139+
return ctrl.Result{}, retErr
69140
}
141+
70142
authSecret := &corev1.Secret{}
71143
if err := r.Get(ctx, types.NamespacedName{
72-
Namespace: gitSync.Spec.AuthRef.Namespace,
73-
Name: gitSync.Spec.AuthRef.Name,
144+
Namespace: obj.Spec.AuthRef.Namespace,
145+
Name: obj.Spec.AuthRef.Name,
74146
}, authSecret); err != nil {
75-
return ctrl.Result{}, fmt.Errorf("failed to find authentication secret: %w", err)
147+
retErr = fmt.Errorf("failed to find authentication secret: %w", err)
148+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.AuthenticateGetFailedReason, retErr.Error())
149+
150+
return ctrl.Result{}, retErr
76151
}
77152

78153
// trim any trailing `/` and then just add.
79154
log.V(4).Info("crafting artifact URL to download from", "url", snapshot.Status.RepositoryURL)
80155
opts := &providers.PushOptions{
81-
URL: gitSync.Spec.URL,
82-
Message: gitSync.Spec.CommitTemplate.Message,
83-
Name: gitSync.Spec.CommitTemplate.Name,
84-
Email: gitSync.Spec.CommitTemplate.Email,
156+
URL: obj.Spec.URL,
157+
Message: obj.Spec.CommitTemplate.Message,
158+
Name: obj.Spec.CommitTemplate.Name,
159+
Email: obj.Spec.CommitTemplate.Email,
85160
Snapshot: snapshot,
86-
Branch: gitSync.Spec.Branch,
87-
SubPath: gitSync.Spec.SubPath,
161+
Branch: obj.Spec.Branch,
162+
SubPath: obj.Spec.SubPath,
88163
}
164+
89165
r.parseAuthSecret(authSecret, opts)
90166

91167
digest, err := r.Git.Push(ctx, opts)
92168
if err != nil {
93-
return ctrl.Result{}, fmt.Errorf("failed to push to git repository: %w", err)
94-
}
95-
// Initialize the patch helper.
96-
patchHelper, err := patch.NewHelper(gitSync, r.Client)
97-
if err != nil {
98-
return ctrl.Result{
99-
RequeueAfter: 1 * time.Minute,
100-
}, fmt.Errorf("failed to create patch helper: %w", err)
101-
}
169+
retErr = fmt.Errorf("failed to push to git repository: %w", err)
170+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.GitRepositoryPushFailedReason, retErr.Error())
102171

103-
gitSync.Status.Digest = digest
104-
if err := patchHelper.Patch(ctx, gitSync); err != nil {
105-
return ctrl.Result{
106-
RequeueAfter: 1 * time.Minute,
107-
}, fmt.Errorf("failed to patch git sync object: %w", err)
172+
return ctrl.Result{}, retErr
108173
}
109-
log.V(4).Info("patch successful")
110174

111-
return ctrl.Result{}, nil
175+
obj.Status.Digest = digest
176+
177+
return result, retErr
112178
}
113179

114180
// SetupWithManager sets up the controller with the Manager.

controllers/gitsync_controller_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"testing"
66

7+
"github.com/fluxcd/pkg/apis/meta"
8+
"github.com/fluxcd/pkg/runtime/conditions"
79
"github.com/stretchr/testify/assert"
810
"github.com/stretchr/testify/require"
911
v1 "k8s.io/api/core/v1"
@@ -85,6 +87,7 @@ func TestGitSyncReconciler(t *testing.T) {
8587
require.NoError(t, err)
8688

8789
assert.Equal(t, "test-digest", gitSync.Status.Digest)
90+
assert.True(t, conditions.IsTrue(gitSync, meta.ReadyCondition))
8891
}
8992

9093
type mockGit struct {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.20
44

55
require (
66
github.com/Masterminds/semver/v3 v3.2.0
7+
github.com/fluxcd/pkg/apis/meta v0.19.0
78
github.com/fluxcd/pkg/runtime v0.27.0
89
github.com/fluxcd/pkg/tar v0.2.0
910
github.com/go-git/go-git/v5 v5.6.0
@@ -65,7 +66,6 @@ require (
6566
github.com/emirpasic/gods v1.18.1 // indirect
6667
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
6768
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
68-
github.com/fluxcd/pkg/apis/meta v0.19.0 // indirect
6969
github.com/fsnotify/fsnotify v1.6.0 // indirect
7070
github.com/fvbommel/sortorder v1.0.2 // indirect
7171
github.com/ghodss/yaml v1.0.0 // indirect

0 commit comments

Comments
 (0)