Skip to content

Commit d20663b

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.
1 parent 03c56c1 commit d20663b

9 files changed

Lines changed: 746 additions & 1006 deletions

api/v1beta1/ansibletest_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,8 @@ func (instance AnsibleTest) RbacNamespace() string {
220220
func (instance AnsibleTest) RbacResourceName() string {
221221
return instance.Name
222222
}
223+
224+
// GetConditions - return the conditions from the status
225+
func (instance *AnsibleTest) GetConditions() *condition.Conditions {
226+
return &instance.Status.Conditions
227+
}

api/v1beta1/horizontest_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,8 @@ func (instance HorizonTest) RbacNamespace() string {
191191
func (instance HorizonTest) RbacResourceName() string {
192192
return instance.Name
193193
}
194+
195+
// GetConditions - return the conditions from the status
196+
func (instance *HorizonTest) GetConditions() *condition.Conditions {
197+
return &instance.Status.Conditions
198+
}

api/v1beta1/tempest_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,8 @@ func (instance Tempest) RbacNamespace() string {
522522
func (instance Tempest) RbacResourceName() string {
523523
return instance.Name
524524
}
525+
526+
// GetConditions - return the conditions from the status
527+
func (instance *Tempest) GetConditions() *condition.Conditions {
528+
return &instance.Status.Conditions
529+
}

api/v1beta1/tobiko_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,8 @@ func (instance Tobiko) RbacNamespace() string {
246246
func (instance Tobiko) RbacResourceName() string {
247247
return instance.Name
248248
}
249+
250+
// GetConditions - return the conditions from the status
251+
func (instance *Tobiko) GetConditions() *condition.Conditions {
252+
return &instance.Status.Conditions
253+
}

internal/controller/ansibletest_controller.go

Lines changed: 61 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,13 @@ package controller
1919

2020
import (
2121
"context"
22-
"fmt"
23-
"strconv"
2422

2523
"github.com/go-logr/logr"
26-
"github.com/openstack-k8s-operators/lib-common/modules/common"
2724
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2825
"github.com/openstack-k8s-operators/lib-common/modules/common/env"
29-
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
3026
testv1beta1 "github.com/openstack-k8s-operators/test-operator/api/v1beta1"
3127
"github.com/openstack-k8s-operators/test-operator/internal/ansibletest"
3228
corev1 "k8s.io/api/core/v1"
33-
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
3429
ctrl "sigs.k8s.io/controller-runtime"
3530
"sigs.k8s.io/controller-runtime/pkg/log"
3631
)
@@ -56,222 +51,84 @@ func (r *AnsibleTestReconciler) GetLogger(ctx context.Context) logr.Logger {
5651
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;create;update;watch;patch;delete
5752

5853
// Reconcile - AnsibleTest
59-
func (r *AnsibleTestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) {
60-
Log := r.GetLogger(ctx)
61-
62-
// Fetch the ansible instance
54+
func (r *AnsibleTestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
6355
instance := &testv1beta1.AnsibleTest{}
64-
err := r.Client.Get(ctx, req.NamespacedName, instance)
65-
if err != nil {
66-
if k8s_errors.IsNotFound(err) {
67-
return ctrl.Result{}, nil
68-
}
69-
return ctrl.Result{}, err
70-
}
71-
72-
// Create a helper
73-
helper, err := helper.NewHelper(
74-
instance,
75-
r.Client,
76-
r.Kclient,
77-
r.Scheme,
78-
r.Log,
79-
)
80-
if err != nil {
81-
return ctrl.Result{}, err
82-
}
83-
84-
// initialize status
85-
isNewInstance := instance.Status.Conditions == nil
86-
if isNewInstance {
87-
instance.Status.Conditions = condition.Conditions{}
88-
}
89-
90-
// Save a copy of the conditions so that we can restore the LastTransitionTime
91-
// when a condition's state doesn't change.
92-
savedConditions := instance.Status.Conditions.DeepCopy()
93-
94-
// Always patch the instance status when exiting this function so we
95-
// can persist any changes.
96-
defer func() {
97-
// Don't update the status, if reconciler Panics
98-
if r := recover(); r != nil {
99-
Log.Info(fmt.Sprintf("panic during reconcile %v\n", r))
100-
panic(r)
101-
}
102-
condition.RestoreLastTransitionTimes(&instance.Status.Conditions, savedConditions)
103-
if instance.Status.Conditions.IsUnknown(condition.ReadyCondition) {
104-
instance.Status.Conditions.Set(
105-
instance.Status.Conditions.Mirror(condition.ReadyCondition))
106-
}
107-
err := helper.PatchInstance(ctx, instance)
108-
if err != nil {
109-
_err = err
110-
return
111-
}
112-
}()
113-
114-
if isNewInstance {
115-
// Initialize conditions used later as Status=Unknown
116-
cl := condition.CreateList(
117-
condition.UnknownCondition(condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage),
118-
condition.UnknownCondition(condition.InputReadyCondition, condition.InitReason, condition.InputReadyInitMessage),
119-
condition.UnknownCondition(condition.DeploymentReadyCondition, condition.InitReason, condition.DeploymentReadyInitMessage),
120-
)
121-
instance.Status.Conditions.Init(&cl)
12256

123-
// Register overall status immediately to have an early feedback
124-
// e.g. in the cli
125-
return ctrl.Result{}, nil
57+
config := FrameworkConfig[*testv1beta1.AnsibleTest]{
58+
ServiceName: ansibletest.ServiceName,
59+
NeedsNetworkAttachments: false,
60+
NeedsConfigMaps: false,
61+
NeedsFinalizer: false,
62+
SupportsWorkflow: true,
63+
64+
BuildPod: func(ctx context.Context, instance *testv1beta1.AnsibleTest, labels, annotations map[string]string, workflowStepNum int, pvcIndex int) (*corev1.Pod, error) {
65+
return r.buildAnsibleTestPod(ctx, instance, labels, annotations, workflowStepNum, pvcIndex)
66+
},
67+
68+
GetInitialConditions: func() []*condition.Condition {
69+
return []*condition.Condition{
70+
condition.UnknownCondition(condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage),
71+
condition.UnknownCondition(condition.InputReadyCondition, condition.InitReason, condition.InputReadyInitMessage),
72+
condition.UnknownCondition(condition.DeploymentReadyCondition, condition.InitReason, condition.DeploymentReadyInitMessage),
73+
}
74+
},
75+
76+
ValidateInputs: func(ctx context.Context, instance *testv1beta1.AnsibleTest) error {
77+
return r.ValidateOpenstackInputs(ctx, instance, instance.Spec.OpenStackConfigMap, instance.Spec.OpenStackConfigSecret)
78+
},
79+
80+
GetSpec: func(instance *testv1beta1.AnsibleTest) interface{} {
81+
return &instance.Spec
82+
},
83+
84+
GetWorkflowStep: func(instance *testv1beta1.AnsibleTest, step int) interface{} {
85+
return instance.Spec.Workflow[step]
86+
},
87+
88+
GetWorkflowLength: func(instance *testv1beta1.AnsibleTest) int {
89+
return len(instance.Spec.Workflow)
90+
},
91+
92+
GetStorageClass: func(instance *testv1beta1.AnsibleTest) string {
93+
return instance.Spec.StorageClass
94+
},
95+
96+
SetObservedGeneration: func(instance *testv1beta1.AnsibleTest) {
97+
instance.Status.ObservedGeneration = instance.Generation
98+
},
12699
}
127-
instance.Status.ObservedGeneration = instance.Generation
128-
129-
workflowLength := len(instance.Spec.Workflow)
130-
nextAction, nextWorkflowStep, err := r.NextAction(ctx, instance, workflowLength)
131-
if nextWorkflowStep < workflowLength {
132-
MergeSections(&instance.Spec, instance.Spec.Workflow[nextWorkflowStep])
133-
}
134-
135-
switch nextAction {
136-
case Failure:
137-
return ctrl.Result{}, err
138-
139-
case Wait:
140-
Log.Info(InfoWaitingOnPod)
141-
return ctrl.Result{RequeueAfter: RequeueAfterValue}, nil
142-
143-
case EndTesting:
144-
// All pods created by the instance were completed. Release the lock
145-
// so that other instances can spawn their pods.
146-
if lockReleased, err := r.ReleaseLock(ctx, instance); !lockReleased {
147-
Log.Info(fmt.Sprintf(InfoCanNotReleaseLock, testOperatorLockName))
148-
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
149-
}
150-
151-
instance.Status.Conditions.MarkTrue(
152-
condition.DeploymentReadyCondition,
153-
condition.DeploymentReadyMessage)
154-
155-
if instance.Status.Conditions.AllSubConditionIsTrue() {
156-
instance.Status.Conditions.MarkTrue(condition.ReadyCondition, condition.ReadyMessage)
157-
}
158-
159-
Log.Info(InfoTestingCompleted)
160-
return ctrl.Result{}, nil
161-
162-
case CreateFirstPod:
163-
lockAcquired, err := r.AcquireLock(ctx, instance, helper, false)
164-
if !lockAcquired {
165-
Log.Info(fmt.Sprintf(InfoCanNotAcquireLock, testOperatorLockName))
166-
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
167-
}
168-
169-
Log.Info(fmt.Sprintf(InfoCreatingFirstPod, nextWorkflowStep))
170-
171-
case CreateNextPod:
172-
// Confirm that we still hold the lock. This is useful to check if for
173-
// example somebody / something deleted the lock and it got claimed by
174-
// another instance. This is considered to be an error state.
175-
lockAcquired, err := r.AcquireLock(ctx, instance, helper, false)
176-
if !lockAcquired {
177-
Log.Error(err, ErrConfirmLockOwnership, testOperatorLockName)
178-
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
179-
}
180100

181-
Log.Info(fmt.Sprintf(InfoCreatingNextPod, nextWorkflowStep))
182-
183-
default:
184-
return ctrl.Result{}, ErrReceivedUnexpectedAction
185-
}
186-
187-
serviceLabels := map[string]string{
188-
common.AppSelector: ansibletest.ServiceName,
189-
workflowStepLabel: strconv.Itoa(nextWorkflowStep),
190-
instanceNameLabel: instance.Name,
191-
operatorNameLabel: "test-operator",
192-
}
193-
194-
err = r.ValidateOpenstackInputs(ctx, instance, instance.Spec.OpenStackConfigMap, instance.Spec.OpenStackConfigSecret)
195-
if err != nil {
196-
instance.Status.Conditions.Set(condition.FalseCondition(
197-
condition.InputReadyCondition,
198-
condition.ErrorReason,
199-
condition.SeverityError,
200-
condition.InputReadyErrorMessage,
201-
err.Error()))
202-
return ctrl.Result{RequeueAfter: RequeueAfterValue}, err
203-
}
204-
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
205-
206-
// Create PersistentVolumeClaim
207-
ctrlResult, err := r.EnsureLogsPVCExists(
208-
ctx,
209-
instance,
210-
helper,
211-
serviceLabels,
212-
instance.Spec.StorageClass,
213-
0,
214-
)
215-
if err != nil {
216-
return ctrlResult, err
217-
} else if (ctrlResult != ctrl.Result{}) {
218-
return ctrlResult, nil
219-
}
220-
// Create PersistentVolumeClaim - end
101+
return CommonReconcile(ctx, &r.Reconciler, req, instance, config, r.GetLogger(ctx))
102+
}
221103

222-
// Create a new pod
104+
func (r *AnsibleTestReconciler) buildAnsibleTestPod(
105+
ctx context.Context,
106+
instance *testv1beta1.AnsibleTest,
107+
labels, _ map[string]string,
108+
workflowStepNum int,
109+
pvcIndex int,
110+
) (*corev1.Pod, error) {
223111
mountCerts := r.CheckSecretExists(ctx, instance, "combined-ca-bundle")
224-
podName := r.GetPodName(instance, nextWorkflowStep)
225112
envVars := r.PrepareAnsibleEnv(instance)
226-
logsPVCName := r.GetPVCLogsName(instance, 0)
113+
114+
podName := r.GetPodName(instance, workflowStepNum)
115+
logsPVCName := r.GetPVCLogsName(instance, pvcIndex)
116+
227117
containerImage, err := r.GetContainerImage(ctx, instance)
228118
if err != nil {
229-
return ctrl.Result{}, err
119+
return nil, err
230120
}
231121

232-
podDef := ansibletest.Pod(
122+
return ansibletest.Pod(
233123
instance,
234-
serviceLabels,
124+
labels,
235125
podName,
236126
logsPVCName,
237127
mountCerts,
238128
envVars,
239-
nextWorkflowStep,
129+
workflowStepNum,
240130
containerImage,
241-
)
242-
243-
ctrlResult, err = r.CreatePod(ctx, *helper, podDef)
244-
if err != nil {
245-
// Creation of the ansibleTests pod was not successful.
246-
// Release the lock and allow other controllers to spawn
247-
// a pod.
248-
if lockReleased, err := r.ReleaseLock(ctx, instance); !lockReleased {
249-
return ctrl.Result{}, err
250-
}
251-
252-
instance.Status.Conditions.Set(condition.FalseCondition(
253-
condition.DeploymentReadyCondition,
254-
condition.ErrorReason,
255-
condition.SeverityWarning,
256-
condition.DeploymentReadyErrorMessage,
257-
err.Error()))
258-
return ctrlResult, err
259-
} else if (ctrlResult != ctrl.Result{}) {
260-
instance.Status.Conditions.Set(condition.FalseCondition(
261-
condition.DeploymentReadyCondition,
262-
condition.RequestedReason,
263-
condition.SeverityInfo,
264-
condition.DeploymentReadyRunningMessage))
265-
return ctrlResult, nil
266-
}
267-
// Create a new pod - end
268-
269-
if instance.Status.Conditions.AllSubConditionIsTrue() {
270-
instance.Status.Conditions.MarkTrue(condition.ReadyCondition, condition.ReadyMessage)
271-
}
272-
273-
Log.Info("Reconciled Service successfully")
274-
return ctrl.Result{}, nil
131+
), nil
275132
}
276133

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

0 commit comments

Comments
 (0)