Skip to content

Commit da40023

Browse files
Merge pull request #428 from kstrenkova/cr-update-restarts-pods
Allow CR updates
2 parents e8c659f + 4bcfe5f commit da40023

11 files changed

Lines changed: 141 additions & 22 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 associated pods 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: 75 additions & 0 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"
@@ -254,6 +255,11 @@ func (r *Reconciler) GetLastPod(
254255
maxPodWorkflowStep := 0
255256

256257
for _, pod := range podList.Items {
258+
// Skip pods that are being deleted
259+
if pod.DeletionTimestamp != nil {
260+
continue
261+
}
262+
257263
workflowStep, err := strconv.Atoi(pod.Labels[workflowStepLabel])
258264
if err != nil {
259265
return &corev1.Pod{}, err
@@ -914,3 +920,72 @@ func MergeSections(main interface{}, workflow interface{}) {
914920
}
915921
}
916922
}
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+
if hash := pod.Annotations["test.openstack.org/config-hash"]; hash != "" {
969+
currentHash = hash
970+
break
971+
}
972+
}
973+
974+
if currentHash == "" || currentHash == newHash {
975+
return ctrl.Result{}, nil
976+
}
977+
978+
for _, pod := range podList.Items {
979+
if pod.DeletionTimestamp != nil {
980+
continue
981+
}
982+
983+
Log.Info("Configuration changed, deleting pod", "pod", pod.Name)
984+
985+
if err := r.Client.Delete(ctx, &pod); err != nil && !k8s_errors.IsNotFound(err) {
986+
return ctrl.Result{}, err
987+
}
988+
}
989+
990+
return ctrl.Result{Requeue: true}, nil
991+
}

internal/controller/common_controller.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ func CommonReconcile[T TestResource](
184184
return ctrl.Result{}, err
185185
}
186186

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+
}
193+
187194
// Apply workflow step overrides to the base spec
188195
if config.SupportsWorkflow && workflowStepIndex < workflowLength {
189196
spec := config.GetSpec(instance)
@@ -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)