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

Commit d9322c0

Browse files
authored
Merge pull request #10 from open-component-model/apply-conditions
feat(ref): add conditions to git-sync-controller
2 parents 3688873 + 89269b5 commit d9322c0

File tree

9 files changed

+245
-41
lines changed

9 files changed

+245
-41
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+
CredentialsNotFoundReason = "CredentialsNotFound"
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

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

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

config/manager/deployment.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v1
22
kind: Namespace
33
metadata:
44
labels:
5-
control-plane: git-sync-controller
5+
app: git-sync-controller
66
name: ocm-system
77
---
88
apiVersion: apps/v1
@@ -11,18 +11,18 @@ metadata:
1111
name: git-sync-controller
1212
namespace: ocm-system
1313
labels:
14-
control-plane: git-sync-controller
14+
app: git-sync-controller
1515
spec:
1616
selector:
1717
matchLabels:
18-
control-plane: git-sync-controller
18+
app: git-sync-controller
1919
replicas: 1
2020
template:
2121
metadata:
2222
annotations:
2323
kubectl.kubernetes.io/default-container: manager
2424
labels:
25-
control-plane: git-sync-controller
25+
app: git-sync-controller
2626
spec:
2727
securityContext:
2828
runAsNonRoot: true

controllers/gitsync_controller.go

Lines changed: 105 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,139 @@ 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)
65+
66+
// The replication controller doesn't need a shouldReconcile, because it should always reconcile,
67+
// that is its purpose.
68+
patchHelper, err := patch.NewHelper(obj, r.Client)
69+
if err != nil {
70+
retErr = errors.Join(retErr, err)
71+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.PatchFailedReason, err.Error())
72+
73+
return ctrl.Result{}, retErr
74+
}
75+
76+
// Always attempt to patch the object and status after each reconciliation.
77+
defer func() {
78+
// Patching has not been set up, or the controller errored earlier.
79+
if patchHelper == nil {
80+
return
81+
}
5782

58-
if gitSync.Status.Digest != "" {
59-
log.Info("GitSync object already synced; status contains digest information", "digest", gitSync.Status.Digest)
83+
if condition := conditions.Get(obj, meta.StalledCondition); condition != nil && condition.Status == metav1.ConditionTrue {
84+
conditions.Delete(obj, meta.ReconcilingCondition)
85+
}
86+
87+
// Check if it's a successful reconciliation.
88+
// We don't set Requeue in case of error, so we can safely check for Requeue.
89+
if result.RequeueAfter == obj.GetRequeueAfter() && !result.Requeue && retErr == nil {
90+
// Remove the reconciling condition if it's set.
91+
conditions.Delete(obj, meta.ReconcilingCondition)
92+
93+
// Set the return err as the ready failure message if the resource is not ready, but also not reconciling or stalled.
94+
if ready := conditions.Get(obj, meta.ReadyCondition); ready != nil && ready.Status == metav1.ConditionFalse && !conditions.IsStalled(obj) {
95+
retErr = errors.New(conditions.GetMessage(obj, meta.ReadyCondition))
96+
}
97+
}
98+
99+
// If still reconciling then reconciliation did not succeed, set to ProgressingWithRetry to
100+
// indicate that reconciliation will be retried.
101+
if conditions.IsReconciling(obj) {
102+
reconciling := conditions.Get(obj, meta.ReconcilingCondition)
103+
reconciling.Reason = meta.ProgressingWithRetryReason
104+
conditions.Set(obj, reconciling)
105+
}
106+
107+
// If not reconciling or stalled than mark Ready=True
108+
if !conditions.IsReconciling(obj) &&
109+
!conditions.IsStalled(obj) &&
110+
retErr == nil {
111+
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "Reconciliation success")
112+
}
113+
114+
// Set status observed generation option if the component is stalled or ready.
115+
if conditions.IsStalled(obj) || conditions.IsReady(obj) {
116+
obj.Status.ObservedGeneration = obj.Generation
117+
}
118+
119+
// Update the object.
120+
if err := patchHelper.Patch(ctx, obj); err != nil {
121+
retErr = errors.Join(retErr, err)
122+
}
123+
}()
124+
125+
// it's important that this happens here so any residual status condition can be overwritten / set.
126+
if obj.Status.Digest != "" {
127+
log.Info("GitSync object already synced; status contains digest information", "digest", obj.Status.Digest)
60128
return ctrl.Result{}, nil
61129
}
62130

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.CredentialsNotFoundReason, 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+
// Remove any stale Ready condition, most likely False, set above. Its value
178+
// is derived from the overall result of the reconciliation in the deferred
179+
// block at the very end.
180+
conditions.Delete(obj, meta.ReadyCondition)
181+
182+
return result, retErr
112183
}
113184

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

0 commit comments

Comments
 (0)