Skip to content

Commit 08b9788

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 08b9788

File tree

3 files changed

+451
-287
lines changed

3 files changed

+451
-287
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: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
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+
)
36+
37+
// FrameworkInstance defines the interface that all test framework CRs must implement
38+
type FrameworkInstance interface {
39+
client.Object
40+
GetConditions() *condition.Conditions
41+
}
42+
43+
// FrameworkConfig defines framework-specific configuration and behavior
44+
type FrameworkConfig[T FrameworkInstance] struct {
45+
// ServiceName for labeling (e.g., "tempest", "tobiko")
46+
ServiceName string
47+
48+
// GetInitialConditions returns the condition list for a new instance
49+
GetInitialConditions func() []*condition.Condition
50+
51+
// NeedsNetworkAttachments indicates if NADs should be handled
52+
NeedsNetworkAttachments bool
53+
54+
// NeedsServiceConfigCondition indicates if ServiceConfigReadyCondition is needed
55+
NeedsServiceConfigCondition bool
56+
57+
// MergeWorkflowStep merges workflow config into the main spec
58+
MergeWorkflowStep func(instance T, workflowStep int)
59+
60+
// GenerateServiceConfigMaps creates framework-specific config maps
61+
GenerateServiceConfigMaps func(ctx context.Context, r *Reconciler, helper *helper.Helper, instance T, workflowStep int) error
62+
63+
// BuildPod creates the framework-specific pod definition
64+
BuildPod func(ctx context.Context, r *Reconciler, instance T, labels, annotations map[string]string, workflowStep int) (*corev1.Pod, error)
65+
66+
// OnReconcileDelete handles deletion logic (optional)
67+
OnReconcileDelete func(ctx context.Context, r *Reconciler, instance T, helper *helper.Helper) (ctrl.Result, error)
68+
69+
// Field accessors
70+
GetWorkflowLength func(instance T) int
71+
GetParallel func(instance T) bool
72+
GetStorageClass func(instance T) string
73+
GetNetworkAttachments func(instance T) []string
74+
GetNetworkAttachmentStatus func(instance T) map[string][]string
75+
SetNetworkAttachmentStatus func(instance T, status map[string][]string)
76+
}
77+
78+
// CommonReconcileWorkflow executes the standard reconciliation workflow using generics
79+
func CommonReconcile[T FrameworkInstance](
80+
ctx context.Context,
81+
r *Reconciler,
82+
req ctrl.Request,
83+
instance T,
84+
config FrameworkConfig[T],
85+
Log logr.Logger,
86+
) (result ctrl.Result, _err error) {
87+
err := r.Client.Get(ctx, req.NamespacedName, instance)
88+
if err != nil {
89+
if k8s_errors.IsNotFound(err) {
90+
return ctrl.Result{}, nil
91+
}
92+
return ctrl.Result{}, err
93+
}
94+
95+
// Create a helper
96+
helper, err := helper.NewHelper(instance, r.Client, r.Kclient, r.Scheme, r.Log)
97+
if err != nil {
98+
return ctrl.Result{}, err
99+
}
100+
101+
// Get conditions from instance
102+
conditions := instance.GetConditions()
103+
if conditions == nil {
104+
return ctrl.Result{}, fmt.Errorf("Instance does not support conditions.")
105+
}
106+
107+
// Initialize status
108+
isNewInstance := len(*conditions) == 0
109+
if isNewInstance {
110+
*conditions = condition.Conditions{}
111+
}
112+
113+
// Save a copy of the condtions so that we can restore the LastTransitionTime
114+
// when a condition's state doesn't change.
115+
savedConditions := conditions.DeepCopy()
116+
117+
// Always patch the instance status when exiting this function so we
118+
// can persist any changes.
119+
defer func() {
120+
// update the overall status condition if service is ready
121+
if conditions.AllSubConditionIsTrue() {
122+
conditions.MarkTrue(condition.ReadyCondition, condition.ReadyMessage)
123+
}
124+
condition.RestoreLastTransitionTimes(conditions, savedConditions)
125+
if conditions.IsUnknown(condition.ReadyCondition) {
126+
conditions.Set(conditions.Mirror(condition.ReadyCondition))
127+
}
128+
err := helper.PatchInstance(ctx, instance)
129+
if err != nil {
130+
_err = err
131+
}
132+
}()
133+
134+
if isNewInstance {
135+
cl := condition.CreateList(config.GetInitialConditions()...)
136+
conditions.Init(&cl)
137+
138+
// Register overall status immediately to have an early feedback
139+
// e.g. in the cli
140+
return ctrl.Result{}, nil
141+
}
142+
143+
// Initialize network attachments status if needed
144+
if config.NeedsNetworkAttachments {
145+
if config.GetNetworkAttachmentStatus(instance) == nil {
146+
config.SetNetworkAttachmentStatus(instance, map[string][]string{})
147+
}
148+
}
149+
150+
// Handle service delete
151+
if !instance.GetDeletionTimestamp().IsZero() {
152+
if config.OnReconcileDelete != nil {
153+
return config.OnReconcileDelete(ctx, r, instance, helper)
154+
}
155+
return ctrl.Result{}, nil
156+
}
157+
158+
workflowLength := config.GetWorkflowLength(instance)
159+
nextAction, workflowStep, err := r.NextAction(ctx, instance, workflowLength)
160+
161+
// Merge workflow step if applicable
162+
if workflowStep < workflowLength && config.MergeWorkflowStep != nil {
163+
config.MergeWorkflowStep(instance, workflowStep)
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.NeedsServiceConfigCondition {
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+
nadObj, err := nad.GetNADWithName(ctx, helper, netAtt, instance.GetNamespace())
315+
if err != nil {
316+
if k8s_errors.IsNotFound(err) {
317+
r.Log.Info(fmt.Sprintf("network-attachment-definition %s not found", netAtt))
318+
conditions.Set(condition.FalseCondition(
319+
condition.NetworkAttachmentsReadyCondition,
320+
condition.ErrorReason,
321+
condition.SeverityWarning,
322+
condition.NetworkAttachmentsReadyWaitingMessage,
323+
netAtt))
324+
return nil, ctrl.Result{RequeueAfter: time.Second * 10}, nil
325+
}
326+
conditions.Set(condition.FalseCondition(
327+
condition.NetworkAttachmentsReadyCondition,
328+
condition.ErrorReason,
329+
condition.SeverityWarning,
330+
condition.NetworkAttachmentsReadyErrorMessage,
331+
err.Error()))
332+
return nil, ctrl.Result{}, err
333+
}
334+
335+
if nadObj != nil {
336+
nadList = append(nadList, *nadObj)
337+
}
338+
}
339+
340+
serviceAnnotations, err := nad.EnsureNetworksAnnotation(nadList)
341+
if err != nil {
342+
return nil, ctrl.Result{}, fmt.Errorf("failed create network annotation from %s: %w",
343+
networkAttachments, err)
344+
}
345+
346+
// Verify network status if pod exists
347+
if r.PodExists(ctx, instance, workflowStep) {
348+
networkReady, networkAttachmentStatus, err := nad.VerifyNetworkStatusFromAnnotation(
349+
ctx, helper, networkAttachments, labels, 1,
350+
)
351+
if err != nil {
352+
return nil, ctrl.Result{}, err
353+
}
354+
355+
config.SetNetworkAttachmentStatus(instance, networkAttachmentStatus)
356+
357+
if networkReady {
358+
conditions.MarkTrue(
359+
condition.NetworkAttachmentsReadyCondition,
360+
condition.NetworkAttachmentsReadyMessage)
361+
} else {
362+
err := fmt.Errorf("%w: %s", ErrNetworkAttachmentsMismatch, networkAttachments)
363+
conditions.Set(condition.FalseCondition(
364+
condition.NetworkAttachmentsReadyCondition,
365+
condition.ErrorReason,
366+
condition.SeverityWarning,
367+
condition.NetworkAttachmentsReadyErrorMessage,
368+
err.Error()))
369+
return nil, ctrl.Result{}, err
370+
}
371+
}
372+
373+
return serviceAnnotations, ctrl.Result{}, nil
374+
}

0 commit comments

Comments
 (0)