Skip to content

Commit 72885c5

Browse files
committed
Allow CR updates
This PR introduces new test-operator functionality that will allow users to update their CRs and simultaneously restart the the pod. Currently, any change to the CR will not apply in the pod. The only way a user can use updated CR is to remove the pod and create a new one.
1 parent 42f48d1 commit 72885c5

11 files changed

Lines changed: 154 additions & 28 deletions

File tree

api/v1beta1/ansibletest_webhook.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ limitations under the License.
2323
package v1beta1
2424

2525
import (
26+
"errors"
27+
2628
"k8s.io/apimachinery/pkg/runtime"
2729
"k8s.io/apimachinery/pkg/util/validation/field"
2830
logf "sigs.k8s.io/controller-runtime/pkg/log"
@@ -74,8 +76,14 @@ func (r *AnsibleTest) ValidateCreate() (admission.Warnings, error) {
7476
func (r *AnsibleTest) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
7577
ansibletestlog.Info("validate update", "name", r.Name)
7678

77-
// TODO(user): fill in your validation logic upon object update.
78-
return nil, nil
79+
oldAnsibleTest, ok := old.(*AnsibleTest)
80+
if !ok || oldAnsibleTest == nil {
81+
return nil, errors.New("unable to convert existing object")
82+
}
83+
84+
allWarnings := admission.Warnings{}
85+
allWarnings = CheckSpecUpdated(allWarnings, oldAnsibleTest.Spec, r.Spec, r.Kind)
86+
return allWarnings, nil
7987
}
8088

8189
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type

api/v1beta1/common_webhook.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"reflect"
66

7+
"github.com/google/go-cmp/cmp"
78
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
89
apierrors "k8s.io/apimachinery/pkg/api/errors"
910
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -75,6 +76,9 @@ const (
7576
"set to true. Please, consider setting %[1]s.Spec.SELinuxLevel. This " +
7677
"ensures that the copying of the logs to the PV is completed without any " +
7778
"complications."
79+
80+
// WarnSpecUpdated
81+
WarnSpecUpdated = "%s CR updated. The pod will be recreated to apply changes."
7882
)
7983

8084
const (
@@ -193,3 +197,11 @@ func BuildValidationError(kind, name string, errs field.ErrorList) error {
193197
}
194198
return nil
195199
}
200+
201+
// CheckSpecUpdated returns warning if spec has changed
202+
func CheckSpecUpdated(allWarn admission.Warnings, oldSpec, newSpec interface{}, kind string) admission.Warnings {
203+
if !cmp.Equal(oldSpec, newSpec) {
204+
allWarn = append(allWarn, fmt.Sprintf(WarnSpecUpdated, kind))
205+
}
206+
return allWarn
207+
}

api/v1beta1/horizontest_webhook.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ limitations under the License.
2323
package v1beta1
2424

2525
import (
26+
"errors"
27+
2628
"k8s.io/apimachinery/pkg/runtime"
2729
logf "sigs.k8s.io/controller-runtime/pkg/log"
2830
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -59,8 +61,14 @@ func (r *HorizonTest) ValidateCreate() (admission.Warnings, error) {
5961
func (r *HorizonTest) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
6062
horizontestlog.Info("validate update", "name", r.Name)
6163

62-
// TODO(user): fill in your validation logic upon object update.
63-
return nil, nil
64+
oldHorizonTest, ok := old.(*HorizonTest)
65+
if !ok || oldHorizonTest == nil {
66+
return nil, errors.New("unable to convert existing object")
67+
}
68+
69+
allWarnings := admission.Warnings{}
70+
allWarnings = CheckSpecUpdated(allWarnings, oldHorizonTest.Spec, r.Spec, r.Kind)
71+
return allWarnings, nil
6472
}
6573

6674
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type

api/v1beta1/tempest_webhook.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"errors"
2727
"fmt"
2828

29-
"github.com/google/go-cmp/cmp"
3029
"k8s.io/apimachinery/pkg/runtime"
3130
"k8s.io/apimachinery/pkg/util/validation/field"
3231
logf "sigs.k8s.io/controller-runtime/pkg/log"
@@ -108,14 +107,9 @@ func (r *Tempest) ValidateUpdate(old runtime.Object) (admission.Warnings, error)
108107
return nil, errors.New("unable to convert existing object")
109108
}
110109

111-
if !cmp.Equal(oldTempest.Spec, r.Spec) {
112-
warnings := admission.Warnings{}
113-
warnings = append(warnings, "You are updating an already existing instance of a "+
114-
"Tempest CR! Be aware that changes won't be applied.")
115-
116-
return warnings, errors.New("updating an existing Tempest CR is not supported")
117-
}
118-
return nil, nil
110+
allWarnings := admission.Warnings{}
111+
allWarnings = CheckSpecUpdated(allWarnings, oldTempest.Spec, r.Spec, r.Kind)
112+
return allWarnings, nil
119113
}
120114

121115
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type

api/v1beta1/tobiko_webhook.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ limitations under the License.
2323
package v1beta1
2424

2525
import (
26+
"errors"
2627
"fmt"
2728

2829
"k8s.io/apimachinery/pkg/runtime"
@@ -82,8 +83,14 @@ func (r *Tobiko) ValidateCreate() (admission.Warnings, error) {
8283
func (r *Tobiko) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
8384
tobikolog.Info("validate update", "name", r.Name)
8485

85-
// TODO(user): fill in your validation logic upon object update.
86-
return nil, nil
86+
oldTobiko, ok := old.(*Tobiko)
87+
if !ok || oldTobiko == nil {
88+
return nil, errors.New("unable to convert existing object")
89+
}
90+
91+
allWarnings := admission.Warnings{}
92+
allWarnings = CheckSpecUpdated(allWarnings, oldTobiko.Spec, r.Spec, r.Kind)
93+
return allWarnings, nil
8794
}
8895

8996
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type

internal/ansibletest/pod.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
func Pod(
1313
instance *testv1beta1.AnsibleTest,
1414
labels map[string]string,
15+
annotations map[string]string,
1516
podName string,
1617
logsPVCName string,
1718
mountCerts bool,
@@ -20,7 +21,7 @@ func Pod(
2021
containerImage string,
2122
) *corev1.Pod {
2223
return util.BuildTestPod(
23-
nil, // No annotations
24+
annotations,
2425
PodCapabilities,
2526
containerImage,
2627
instance.Name,

internal/controller/ansibletest_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (r *AnsibleTestReconciler) Reconcile(ctx context.Context, req ctrl.Request)
9696
func (r *AnsibleTestReconciler) buildAnsibleTestPod(
9797
ctx context.Context,
9898
instance *testv1beta1.AnsibleTest,
99-
labels, _ map[string]string,
99+
labels, annotations map[string]string,
100100
workflowStepIndex int,
101101
pvcIndex int,
102102
) (*corev1.Pod, error) {
@@ -114,6 +114,7 @@ func (r *AnsibleTestReconciler) buildAnsibleTestPod(
114114
return ansibletest.Pod(
115115
instance,
116116
labels,
117+
annotations,
117118
podName,
118119
logsPVCName,
119120
mountCerts,

internal/controller/common.go

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controller
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"reflect"
@@ -31,6 +32,7 @@ import (
3132
ctrl "sigs.k8s.io/controller-runtime"
3233
"sigs.k8s.io/controller-runtime/pkg/client"
3334
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
35+
"sigs.k8s.io/controller-runtime/pkg/log"
3436
)
3537

3638
const (
@@ -253,6 +255,11 @@ func (r *Reconciler) GetLastPod(
253255
maxPodWorkflowStep := 0
254256

255257
for _, pod := range podList.Items {
258+
// Skip pods that are being deleted
259+
if pod.DeletionTimestamp != nil {
260+
continue
261+
}
262+
256263
workflowStep, err := strconv.Atoi(pod.Labels[workflowStepLabel])
257264
if err != nil {
258265
return &corev1.Pod{}, err
@@ -489,8 +496,8 @@ func (r *Reconciler) EnsureLogsPVCExists(
489496
}
490497

491498
// GetLogger returns the logger instance
492-
func (r *Reconciler) GetLogger() logr.Logger {
493-
return r.Log
499+
func (r *Reconciler) GetLogger(ctx context.Context) logr.Logger {
500+
return log.FromContext(ctx)
494501
}
495502

496503
// GetScheme returns the runtime scheme
@@ -555,7 +562,7 @@ func (r *Reconciler) AcquireLock(
555562

556563
// ReleaseLock releases the lock for the given instance
557564
func (r *Reconciler) ReleaseLock(ctx context.Context, instance client.Object) (bool, error) {
558-
Log := r.GetLogger()
565+
Log := r.GetLogger(ctx)
559566

560567
cm, err := r.GetLockInfo(ctx, instance)
561568
if err != nil && k8s_errors.IsNotFound(err) {
@@ -913,3 +920,78 @@ func MergeSections(main interface{}, workflow interface{}) {
913920
}
914921
}
915922
}
923+
924+
// CalculateConfigHash calculates a hash of the entire Spec to detect any changes
925+
func CalculateConfigHash(instance client.Object) string {
926+
v := reflect.ValueOf(instance)
927+
spec, err := SafetyCheck(v, "Spec")
928+
if err != nil {
929+
return ""
930+
}
931+
932+
data, err := json.Marshal(spec.Interface())
933+
if err != nil {
934+
return ""
935+
}
936+
937+
hash := sha256.Sum256(data)
938+
return fmt.Sprintf("%x", hash[:8])
939+
}
940+
941+
// CheckConfigChange checks if the spec has changed and recreates all pods related to the instance if needed
942+
func (r *Reconciler) CheckConfigChange(
943+
ctx context.Context,
944+
instance client.Object,
945+
newHash string,
946+
) (ctrl.Result, error) {
947+
Log := r.GetLogger(ctx)
948+
949+
if newHash == "" {
950+
return ctrl.Result{}, nil
951+
}
952+
953+
labels := map[string]string{instanceNameLabel: instance.GetName()}
954+
podList := &corev1.PodList{}
955+
err := r.Client.List(ctx, podList,
956+
client.InNamespace(instance.GetNamespace()),
957+
client.MatchingLabels(labels))
958+
if err != nil {
959+
return ctrl.Result{}, err
960+
}
961+
962+
var currentHash string
963+
for _, pod := range podList.Items {
964+
if pod.DeletionTimestamp != nil {
965+
continue
966+
}
967+
968+
currentHash = pod.Annotations["test.openstack.org/config-hash"]
969+
if currentHash != "" && currentHash != newHash {
970+
break
971+
}
972+
currentHash = ""
973+
}
974+
975+
if currentHash == "" || currentHash == newHash {
976+
return ctrl.Result{}, nil
977+
}
978+
979+
Log.Info("Configuration changed, deleting all related pods for recreation",
980+
"instance", instance.GetName(),
981+
"currentHash", currentHash,
982+
"newHash", newHash)
983+
984+
for _, pod := range podList.Items {
985+
if pod.DeletionTimestamp != nil {
986+
continue
987+
}
988+
989+
Log.Info("Deleting pod", "pod", pod.Name)
990+
991+
if err := r.Client.Delete(ctx, &pod); err != nil && !k8s_errors.IsNotFound(err) {
992+
return ctrl.Result{}, err
993+
}
994+
}
995+
996+
return ctrl.Result{Requeue: true}, nil
997+
}

internal/controller/common_controller.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ func CommonReconcile[T TestResource](
180180
}
181181

182182
nextAction, workflowStepIndex, err := r.NextAction(ctx, instance, workflowLength)
183+
if nextAction == Failure {
184+
return ctrl.Result{}, err
185+
}
186+
187+
// Check for config changes and handle pod recreation
188+
configHash := CalculateConfigHash(instance)
189+
ctrlResult, err := r.CheckConfigChange(ctx, instance, configHash)
190+
if err != nil || (ctrlResult != ctrl.Result{}) {
191+
return ctrlResult, err
192+
}
183193

184194
// Apply workflow step overrides to the base spec
185195
if config.SupportsWorkflow && workflowStepIndex < workflowLength {
@@ -200,9 +210,6 @@ func CommonReconcile[T TestResource](
200210
}
201211

202212
switch nextAction {
203-
case Failure:
204-
return ctrl.Result{}, err
205-
206213
case Wait:
207214
Log.Info(InfoWaitingOnPod)
208215
return ctrl.Result{RequeueAfter: RequeueAfterValue}, nil
@@ -271,7 +278,7 @@ func CommonReconcile[T TestResource](
271278
}
272279

273280
// Create PersistentVolumeClaim
274-
ctrlResult, err := r.EnsureLogsPVCExists(
281+
ctrlResult, err = r.EnsureLogsPVCExists(
275282
ctx,
276283
instance,
277284
helper,
@@ -285,6 +292,9 @@ func CommonReconcile[T TestResource](
285292
return ctrlResult, nil
286293
}
287294

295+
serviceAnnotations := make(map[string]string)
296+
serviceAnnotations["test.openstack.org/config-hash"] = configHash
297+
288298
// Generate ConfigMaps containing test configuration
289299
if config.NeedsConfigMaps {
290300
err = config.GenerateServiceConfigMaps(ctx, helper, serviceLabels, instance, workflowStepIndex)
@@ -300,7 +310,6 @@ func CommonReconcile[T TestResource](
300310
conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage)
301311
}
302312

303-
var serviceAnnotations map[string]string
304313
if config.NeedsNetworkAttachments {
305314
annotations, ctrlResult, err := r.EnsureNetworkAttachments(
306315
ctx,
@@ -313,7 +322,9 @@ func CommonReconcile[T TestResource](
313322
if err != nil || (ctrlResult != ctrl.Result{}) {
314323
return ctrlResult, err
315324
}
316-
serviceAnnotations = annotations
325+
for k, v := range annotations {
326+
serviceAnnotations[k] = v
327+
}
317328
}
318329

319330
// Build pod

internal/controller/horizontest_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (r *HorizonTestReconciler) Reconcile(ctx context.Context, req ctrl.Request)
9696
func (r *HorizonTestReconciler) buildHorizonTestPod(
9797
ctx context.Context,
9898
instance *testv1beta1.HorizonTest,
99-
labels, _ map[string]string,
99+
labels, annotations map[string]string,
100100
workflowStepIndex int,
101101
pvcIndex int,
102102
) (*corev1.Pod, error) {
@@ -115,6 +115,7 @@ func (r *HorizonTestReconciler) buildHorizonTestPod(
115115
return horizontest.Pod(
116116
instance,
117117
labels,
118+
annotations,
118119
podName,
119120
logsPVCName,
120121
mountCerts,

0 commit comments

Comments
 (0)