Skip to content

Commit 30261b6

Browse files
committed
Introduce common controller
To remove duplicate code across the test-operator controllers, this patch introduces a common controller. This change should make fixing controller bugs easier and prepare the test-operator for new resource addition, if it is needed in the future. Assisted-by: Claude AI
1 parent 2e74e89 commit 30261b6

File tree

4 files changed

+459
-306
lines changed

4 files changed

+459
-306
lines changed

api/v1beta1/tempest_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,8 @@ func (instance Tempest) RbacNamespace() string {
529529
func (instance Tempest) RbacResourceName() string {
530530
return instance.Name
531531
}
532+
533+
// GetConditions - return the conditions from the status
534+
func (instance *Tempest) GetConditions() *condition.Conditions {
535+
return &instance.Status.Conditions
536+
}

controllers/common_controller.go

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
/*
2+
Copyright 2023.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strconv"
23+
"time"
24+
25+
"github.com/go-logr/logr"
26+
networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
27+
"github.com/openstack-k8s-operators/lib-common/modules/common"
28+
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
29+
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
30+
nad "github.com/openstack-k8s-operators/lib-common/modules/common/networkattachment"
31+
corev1 "k8s.io/api/core/v1"
32+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
33+
ctrl "sigs.k8s.io/controller-runtime"
34+
"sigs.k8s.io/controller-runtime/pkg/client"
35+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
36+
)
37+
38+
// FrameworkInstance defines the interface that all test framework CRs must implement
39+
type FrameworkInstance interface {
40+
client.Object
41+
GetConditions() *condition.Conditions
42+
}
43+
44+
// FrameworkConfig defines framework-specific configuration and behavior
45+
type FrameworkConfig[T FrameworkInstance] struct {
46+
// ServiceName for labeling (e.g., "tempest", "tobiko")
47+
ServiceName string
48+
49+
// NeedsNetworkAttachments indicates if NADs should be handled
50+
NeedsNetworkAttachments bool
51+
52+
// NeedsConfigMaps indicates if ServiceConfigReadyCondition is needed
53+
NeedsConfigMaps bool
54+
55+
// GenerateServiceConfigMaps creates framework-specific config maps
56+
GenerateServiceConfigMaps func(ctx context.Context, r *Reconciler, helper *helper.Helper, instance T, workflowStep int) error
57+
58+
// BuildPod creates the framework-specific pod definition
59+
BuildPod func(ctx context.Context, r *Reconciler, instance T, labels, annotations map[string]string, workflowStep int) (*corev1.Pod, error)
60+
61+
// GetInitialConditions returns the condition list for a new instance
62+
GetInitialConditions func() []*condition.Condition
63+
64+
// Field accessors
65+
GetWorkflowLength func(instance T) int
66+
GetParallel func(instance T) bool
67+
GetStorageClass func(instance T) string
68+
GetNetworkAttachments func(instance T) []string
69+
GetNetworkAttachmentStatus func(instance T) map[string][]string
70+
SetNetworkAttachmentStatus func(instance T, status map[string][]string)
71+
72+
GetSpec func(instance T) interface{} // Optional
73+
GetWorkflowStep func(instance T, step int) interface{} // Optional
74+
}
75+
76+
// CommonReconcile executes the standard reconciliation workflow using generics
77+
func CommonReconcile[T FrameworkInstance](
78+
ctx context.Context,
79+
r *Reconciler,
80+
req ctrl.Request,
81+
instance T,
82+
config FrameworkConfig[T],
83+
Log logr.Logger,
84+
) (result ctrl.Result, _err error) {
85+
err := r.Client.Get(ctx, req.NamespacedName, instance)
86+
if err != nil {
87+
if k8s_errors.IsNotFound(err) {
88+
return ctrl.Result{}, nil
89+
}
90+
return ctrl.Result{}, err
91+
}
92+
93+
// Create a helper
94+
helper, err := helper.NewHelper(instance, r.Client, r.Kclient, r.Scheme, r.Log)
95+
if err != nil {
96+
return ctrl.Result{}, err
97+
}
98+
99+
// Get conditions from instance
100+
conditions := instance.GetConditions()
101+
if conditions == nil {
102+
return ctrl.Result{}, nil // TODO fmt.Errorf("instance does not support conditions")
103+
}
104+
105+
// Initialize status
106+
isNewInstance := len(*conditions) == 0
107+
if isNewInstance {
108+
*conditions = condition.Conditions{}
109+
}
110+
111+
// Save a copy of the condtions so that we can restore the LastTransitionTime
112+
// when a condition's state doesn't change.
113+
savedConditions := conditions.DeepCopy()
114+
115+
// Always patch the instance status when exiting this function so we
116+
// can persist any changes.
117+
defer func() {
118+
// update the overall status condition if service is ready
119+
if conditions.AllSubConditionIsTrue() {
120+
conditions.MarkTrue(condition.ReadyCondition, condition.ReadyMessage)
121+
}
122+
condition.RestoreLastTransitionTimes(conditions, savedConditions)
123+
if conditions.IsUnknown(condition.ReadyCondition) {
124+
conditions.Set(conditions.Mirror(condition.ReadyCondition))
125+
}
126+
err := helper.PatchInstance(ctx, instance)
127+
if err != nil {
128+
_err = err
129+
}
130+
}()
131+
132+
if isNewInstance {
133+
cl := condition.CreateList(config.GetInitialConditions()...)
134+
conditions.Init(&cl)
135+
136+
// Register overall status immediately to have an early feedback
137+
// e.g. in the cli
138+
return ctrl.Result{}, nil
139+
}
140+
141+
// Initialize network attachments status if needed
142+
if config.NeedsNetworkAttachments {
143+
if config.GetNetworkAttachmentStatus(instance) == nil {
144+
config.SetNetworkAttachmentStatus(instance, map[string][]string{})
145+
}
146+
}
147+
148+
// Handle service delete
149+
if !instance.GetDeletionTimestamp().IsZero() {
150+
Log.Info("Reconciling Service delete")
151+
controllerutil.RemoveFinalizer(instance, helper.GetFinalizer())
152+
Log.Info("Reconciled Service delete successfully")
153+
return ctrl.Result{}, nil
154+
}
155+
156+
workflowLength := config.GetWorkflowLength(instance)
157+
nextAction, workflowStep, err := r.NextAction(ctx, instance, workflowLength)
158+
159+
// Merge workflow step if applicable
160+
if workflowLength != 0 && workflowStep < workflowLength {
161+
spec := config.GetSpec(instance)
162+
workflowStepData := config.GetWorkflowStep(instance, workflowStep)
163+
MergeSections(spec, workflowStepData)
164+
}
165+
166+
switch nextAction {
167+
case Failure:
168+
return ctrl.Result{}, err
169+
170+
case Wait:
171+
Log.Info(InfoWaitingOnPod)
172+
return ctrl.Result{RequeueAfter: RequeueAfterValue}, nil
173+
174+
case EndTesting:
175+
// All pods created by the instance were completed. Release the lock
176+
// so that other instances can spawn their pods.
177+
if lockReleased, err := r.ReleaseLock(ctx, instance); !lockReleased {
178+
Log.Info(fmt.Sprintf(InfoCanNotReleaseLock, testOperatorLockName))
179+
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
180+
}
181+
182+
conditions.MarkTrue(condition.DeploymentReadyCondition, condition.DeploymentReadyMessage)
183+
Log.Info(InfoTestingCompleted)
184+
return ctrl.Result{}, nil
185+
186+
case CreateFirstPod:
187+
lockAcquired, err := r.AcquireLock(ctx, instance, helper, config.GetParallel(instance))
188+
if !lockAcquired {
189+
Log.Info(fmt.Sprintf(InfoCanNotAcquireLock, testOperatorLockName))
190+
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
191+
}
192+
193+
Log.Info(fmt.Sprintf(InfoCreatingFirstPod, workflowStep))
194+
195+
case CreateNextPod:
196+
// Confirm that we still hold the lock. This is useful to check if for
197+
// example somebody / something deleted the lock and it got claimed by
198+
// another instance. This is considered to be an error state.
199+
lockAcquired, err := r.AcquireLock(ctx, instance, helper, config.GetParallel(instance))
200+
if !lockAcquired {
201+
Log.Error(err, ErrConfirmLockOwnership, testOperatorLockName)
202+
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
203+
}
204+
205+
Log.Info(fmt.Sprintf(InfoCreatingNextPod, workflowStep))
206+
207+
default:
208+
return ctrl.Result{}, ErrReceivedUnexpectedAction
209+
}
210+
211+
serviceLabels := map[string]string{
212+
common.AppSelector: config.ServiceName,
213+
workflowStepLabel: strconv.Itoa(workflowStep),
214+
instanceNameLabel: instance.GetName(),
215+
operatorNameLabel: "test-operator",
216+
}
217+
218+
workflowStepNum := 0
219+
// Create multiple PVCs for parallel execution
220+
if config.GetParallel(instance) && workflowStep < config.GetWorkflowLength(instance) {
221+
workflowStepNum = workflowStep
222+
}
223+
224+
// Create PersistentVolumeClaim
225+
ctrlResult, err := r.EnsureLogsPVCExists(
226+
ctx,
227+
instance,
228+
helper,
229+
serviceLabels,
230+
config.GetStorageClass(instance),
231+
workflowStepNum,
232+
)
233+
if err != nil {
234+
return ctrlResult, err
235+
} else if (ctrlResult != ctrl.Result{}) {
236+
return ctrlResult, nil
237+
}
238+
239+
// Generate ConfigMaps if needed
240+
if config.NeedsConfigMaps {
241+
if err = config.GenerateServiceConfigMaps(ctx, r, helper, instance, workflowStep); err != nil {
242+
conditions.Set(condition.FalseCondition(
243+
condition.ServiceConfigReadyCondition,
244+
condition.ErrorReason,
245+
condition.SeverityWarning,
246+
condition.ServiceConfigReadyErrorMessage,
247+
err.Error()))
248+
return ctrl.Result{}, err
249+
}
250+
conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage)
251+
}
252+
// Generate ConfigMaps - end
253+
254+
// Handle network attachments if needed
255+
var serviceAnnotations map[string]string
256+
if config.NeedsNetworkAttachments {
257+
annotations, ctrlResult, err := handleNetworkAttachments(
258+
ctx, r, instance, helper, serviceLabels, config, workflowStep, conditions,
259+
)
260+
if err != nil || (ctrlResult != ctrl.Result{}) {
261+
return ctrlResult, err
262+
}
263+
serviceAnnotations = annotations
264+
}
265+
266+
// Build pod
267+
podDef, err := config.BuildPod(ctx, r, instance, serviceLabels, serviceAnnotations, workflowStep)
268+
if err != nil {
269+
return ctrl.Result{}, err
270+
}
271+
272+
// Create a new pod
273+
ctrlResult, err = r.CreatePod(ctx, *helper, podDef)
274+
if err != nil {
275+
// Release lock on failure
276+
if lockReleased, lockErr := r.ReleaseLock(ctx, instance); lockReleased {
277+
return ctrl.Result{RequeueAfter: RequeueAfterValue}, lockErr
278+
}
279+
280+
conditions.Set(condition.FalseCondition(
281+
condition.DeploymentReadyCondition,
282+
condition.ErrorReason,
283+
condition.SeverityWarning,
284+
condition.DeploymentReadyErrorMessage,
285+
err.Error()))
286+
return ctrlResult, err
287+
} else if (ctrlResult != ctrl.Result{}) {
288+
conditions.Set(condition.FalseCondition(
289+
condition.DeploymentReadyCondition,
290+
condition.RequestedReason,
291+
condition.SeverityInfo,
292+
condition.DeploymentReadyRunningMessage))
293+
return ctrlResult, nil
294+
}
295+
// Create a new pod - end
296+
297+
return ctrl.Result{}, nil
298+
}
299+
300+
func handleNetworkAttachments[T FrameworkInstance](
301+
ctx context.Context,
302+
r *Reconciler,
303+
instance T,
304+
helper *helper.Helper,
305+
labels map[string]string,
306+
config FrameworkConfig[T],
307+
workflowStep int,
308+
conditions *condition.Conditions,
309+
) (map[string]string, ctrl.Result, error) {
310+
nadList := []networkv1.NetworkAttachmentDefinition{}
311+
networkAttachments := config.GetNetworkAttachments(instance)
312+
313+
for _, netAtt := range networkAttachments {
314+
nad, err := nad.GetNADWithName(ctx, helper, netAtt, instance.GetNamespace())
315+
if err != nil {
316+
if k8s_errors.IsNotFound(err) {
317+
// Since the net-attach-def CR should have been manually created by the user and referenced in the spec,
318+
// we treat this as a warning because it means that the service will not be able to start.
319+
r.Log.Info(fmt.Sprintf("network-attachment-definition %s not found", netAtt))
320+
conditions.Set(condition.FalseCondition(
321+
condition.NetworkAttachmentsReadyCondition,
322+
condition.ErrorReason,
323+
condition.SeverityWarning,
324+
condition.NetworkAttachmentsReadyWaitingMessage,
325+
netAtt))
326+
return nil, ctrl.Result{RequeueAfter: time.Second * 10}, nil
327+
}
328+
conditions.Set(condition.FalseCondition(
329+
condition.NetworkAttachmentsReadyCondition,
330+
condition.ErrorReason,
331+
condition.SeverityWarning,
332+
condition.NetworkAttachmentsReadyErrorMessage,
333+
err.Error()))
334+
return nil, ctrl.Result{}, err
335+
}
336+
337+
if nad != nil {
338+
nadList = append(nadList, *nad)
339+
}
340+
}
341+
342+
serviceAnnotations, err := nad.EnsureNetworksAnnotation(nadList)
343+
if err != nil {
344+
return nil, ctrl.Result{}, fmt.Errorf("failed create network annotation from %s: %w",
345+
networkAttachments, err)
346+
}
347+
348+
// Verify network status if pod exists
349+
if r.PodExists(ctx, instance, workflowStep) {
350+
networkReady, networkAttachmentStatus, err := nad.VerifyNetworkStatusFromAnnotation(
351+
ctx,
352+
helper,
353+
networkAttachments,
354+
labels,
355+
1,
356+
)
357+
if err != nil {
358+
return nil, ctrl.Result{}, err
359+
}
360+
361+
config.SetNetworkAttachmentStatus(instance, networkAttachmentStatus)
362+
363+
if networkReady {
364+
conditions.MarkTrue(
365+
condition.NetworkAttachmentsReadyCondition,
366+
condition.NetworkAttachmentsReadyMessage)
367+
} else {
368+
err := fmt.Errorf("%w: %s", ErrNetworkAttachmentsMismatch, networkAttachments)
369+
conditions.Set(condition.FalseCondition(
370+
condition.NetworkAttachmentsReadyCondition,
371+
condition.ErrorReason,
372+
condition.SeverityWarning,
373+
condition.NetworkAttachmentsReadyErrorMessage,
374+
err.Error()))
375+
return nil, ctrl.Result{}, err
376+
}
377+
}
378+
379+
return serviceAnnotations, ctrl.Result{}, nil
380+
}

0 commit comments

Comments
 (0)