Skip to content

Commit feb6c2f

Browse files
Faster approvals for the auto-approve controller (#329)
There is now no longer a default delay; it only delays if you specifically give a delay annotation. The minimum delay is now 0. Re-ordered the checks so that the delay will not come into play unless all other checks are already met. Added some tests.
1 parent 1d5a6bc commit feb6c2f

File tree

2 files changed

+197
-38
lines changed

2 files changed

+197
-38
lines changed

controllers/pkg/reconcilers/approval/reconciler.go

+48-38
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import (
4646

4747
const (
4848
DelayAnnotationName = "approval.nephio.org/delay"
49-
DelayConditionType = "approval.nephio.org.DelayExpired"
5049
PolicyAnnotationName = "approval.nephio.org/policy"
5150
InitialPolicyAnnotationValue = "initial"
5251
)
@@ -104,44 +103,16 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
104103
return ctrl.Result{}, nil
105104
}
106105

107-
// If it is published, ignore it
108-
if porchv1alpha1.LifecycleIsPublished(pr.Spec.Lifecycle) {
109-
return ctrl.Result{}, nil
110-
}
111-
112-
// Delay if needed
113-
// This is a workaround for some "settling" that seems to be needed
114-
// in Porch and/or PackageVariant. We should be able to remove it if
115-
// we can fix that.
116-
requeue, err := r.manageDelay(ctx, pr)
117-
if err != nil {
118-
r.recorder.Eventf(pr, corev1.EventTypeWarning,
119-
"Error", "error processing %q: %s", DelayAnnotationName, err.Error())
120-
121-
return ctrl.Result{}, err
122-
}
123-
124-
// if requeue is > 0, then we should do nothing more with this PackageRevision
125-
// for at least that long
126-
if requeue > 0 {
127-
r.recorder.Event(pr, corev1.EventTypeNormal,
128-
"NotApproved", "delay time not met")
129-
return ctrl.Result{RequeueAfter: requeue}, nil
130-
}
131-
132-
// Check for the approval policy annotation
133-
policy, ok := pr.GetAnnotations()[PolicyAnnotationName]
106+
// If we shouldn't process this at all, just return
107+
policy, ok := shouldProcess(pr)
134108
if !ok {
135-
// no policy set, so just return, we are done
136109
return ctrl.Result{}, nil
137110
}
138111

139112
// If the package revision is owned by a PackageVariant, check the Ready condition
140113
// of the package variant. If it is not Ready, then we should not approve yet. The
141114
// lack of readiness could indicate an error which even impacts whether or not the
142115
// readiness gates have been properly set.
143-
//
144-
//
145116
pvReady, err := porchutil.PackageVariantReady(ctx, pr, r.porchClient)
146117
if err != nil {
147118
r.recorder.Event(pr, corev1.EventTypeWarning,
@@ -192,7 +163,32 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
192163
return ctrl.Result{}, nil
193164
}
194165

195-
// policy met
166+
// Delay if needed, and let the user know via an event
167+
// We should be able to get rid of this if we add a policy to check
168+
// the specializer condition. We need to check the *specific* condition,
169+
// because if the condition has not been added to the readiness gates yet,
170+
// we could pass all the gates even though that specific condition is missing.
171+
// That check shouldn't be needed if the initial clone creates the readiness gate
172+
// entry though (with the function pipeline run).
173+
requeue, err := manageDelay(pr)
174+
if err != nil {
175+
r.recorder.Eventf(pr, corev1.EventTypeWarning,
176+
"Error", "error processing %q: %s", DelayAnnotationName, err.Error())
177+
178+
// Do not propagate the error; we do not want it to force an immediate requeue
179+
// If we could not parse the annotation, it is a user error
180+
return ctrl.Result{}, nil
181+
}
182+
183+
// if requeue is > 0, then we should do nothing more with this PackageRevision
184+
// for at least that long
185+
if requeue > 0 {
186+
r.recorder.Event(pr, corev1.EventTypeNormal,
187+
"NotApproved", "delay time not met")
188+
return ctrl.Result{RequeueAfter: requeue}, nil
189+
}
190+
191+
// All policies met
196192
if pr.Spec.Lifecycle == porchv1alpha1.PackageRevisionLifecycleDraft {
197193
pr.Spec.Lifecycle = porchv1alpha1.PackageRevisionLifecycleProposed
198194
err = r.Update(ctx, pr)
@@ -211,19 +207,33 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
211207
return ctrl.Result{}, err
212208
}
213209

214-
func (r *reconciler) manageDelay(ctx context.Context, pr *porchv1alpha1.PackageRevision) (time.Duration, error) {
210+
func shouldProcess(pr *porchv1alpha1.PackageRevision) (string, bool) {
211+
result := true
212+
213+
// If it is published, ignore it
214+
result = result && !porchv1alpha1.LifecycleIsPublished(pr.Spec.Lifecycle)
215+
216+
// Check for the approval policy annotation
217+
policy, ok := pr.GetAnnotations()[PolicyAnnotationName]
218+
result = result && ok
219+
220+
return policy, result
221+
}
222+
223+
func manageDelay(pr *porchv1alpha1.PackageRevision) (time.Duration, error) {
215224
delay, ok := pr.GetAnnotations()[DelayAnnotationName]
216225
if !ok {
217-
delay = "2m"
226+
// only delay if there is a delay annotation
227+
return 0, nil
218228
}
229+
219230
d, err := time.ParseDuration(delay)
220231
if err != nil {
221-
return 0, fmt.Errorf("error parsing delay duration: %w", err)
232+
return 0, err
222233
}
223234

224-
// force at least a 30 second delay
225-
if d < 30*time.Second {
226-
d = 30 * time.Second
235+
if d < 0 {
236+
return 0, fmt.Errorf("invalid delay %q; delay must be 0 or more", delay)
227237
}
228238

229239
if time.Since(pr.CreationTimestamp.Time) > d {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2023 The Nephio Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package approval
16+
17+
import (
18+
"testing"
19+
"time"
20+
21+
porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
22+
"github.com/stretchr/testify/require"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
)
25+
26+
func TestShouldProcess(t *testing.T) {
27+
testCases := map[string]struct {
28+
pr porchapi.PackageRevision
29+
expectedPolicy string
30+
expectedShould bool
31+
}{
32+
"draft with no annotation": {
33+
pr: porchapi.PackageRevision{},
34+
expectedPolicy: "",
35+
expectedShould: false,
36+
},
37+
"draft with policy annotation": {
38+
pr: porchapi.PackageRevision{
39+
ObjectMeta: metav1.ObjectMeta{
40+
Annotations: map[string]string{
41+
"approval.nephio.org/policy": "initial",
42+
},
43+
},
44+
},
45+
expectedPolicy: "initial",
46+
expectedShould: true,
47+
},
48+
"draft with no policy annotation, but delay annotation": {
49+
pr: porchapi.PackageRevision{
50+
ObjectMeta: metav1.ObjectMeta{
51+
Annotations: map[string]string{
52+
"approval.nephio.org/delay": "20s",
53+
},
54+
},
55+
},
56+
expectedPolicy: "",
57+
expectedShould: false,
58+
},
59+
"published with policy annotation": {
60+
pr: porchapi.PackageRevision{
61+
ObjectMeta: metav1.ObjectMeta{
62+
Annotations: map[string]string{
63+
"approval.nephio.org/policy": "initial",
64+
},
65+
},
66+
Spec: porchapi.PackageRevisionSpec{
67+
Lifecycle: "Published",
68+
},
69+
},
70+
expectedPolicy: "initial",
71+
expectedShould: false,
72+
},
73+
}
74+
for tn, tc := range testCases {
75+
t.Run(tn, func(t *testing.T) {
76+
actualPolicy, actualShould := shouldProcess(&tc.pr)
77+
require.Equal(t, tc.expectedPolicy, actualPolicy)
78+
require.Equal(t, tc.expectedShould, actualShould)
79+
})
80+
}
81+
}
82+
83+
func TestManageDelay(t *testing.T) {
84+
now := time.Now()
85+
testCases := map[string]struct {
86+
pr porchapi.PackageRevision
87+
expectedRequeue bool
88+
expectedError bool
89+
}{
90+
"no annotation": {
91+
pr: porchapi.PackageRevision{},
92+
expectedRequeue: false,
93+
expectedError: false,
94+
},
95+
"unparseable annotation": {
96+
pr: porchapi.PackageRevision{
97+
ObjectMeta: metav1.ObjectMeta{
98+
Annotations: map[string]string{
99+
"approval.nephio.org/delay": "foo",
100+
},
101+
},
102+
},
103+
expectedRequeue: false,
104+
expectedError: true,
105+
},
106+
"negative annotation": {
107+
pr: porchapi.PackageRevision{
108+
ObjectMeta: metav1.ObjectMeta{
109+
Annotations: map[string]string{
110+
"approval.nephio.org/delay": "-5s",
111+
},
112+
},
113+
},
114+
expectedRequeue: false,
115+
expectedError: true,
116+
},
117+
"not old enough": {
118+
pr: porchapi.PackageRevision{
119+
ObjectMeta: metav1.ObjectMeta{
120+
CreationTimestamp: metav1.Time{now},
121+
Annotations: map[string]string{
122+
"approval.nephio.org/delay": "1h",
123+
},
124+
},
125+
},
126+
expectedRequeue: true,
127+
expectedError: false,
128+
},
129+
"old enough": {
130+
pr: porchapi.PackageRevision{
131+
ObjectMeta: metav1.ObjectMeta{
132+
CreationTimestamp: metav1.Time{now.AddDate(-1,0,0)},
133+
Annotations: map[string]string{
134+
"approval.nephio.org/delay": "1h",
135+
},
136+
},
137+
},
138+
expectedRequeue: false,
139+
expectedError: false,
140+
},
141+
}
142+
for tn, tc := range testCases {
143+
t.Run(tn, func(t *testing.T) {
144+
actualRequeue, actualError := manageDelay(&tc.pr)
145+
require.Equal(t, tc.expectedRequeue, actualRequeue > 0)
146+
require.Equal(t, tc.expectedError, actualError != nil)
147+
})
148+
}
149+
}

0 commit comments

Comments
 (0)