/* Copyright 2021 The Rook Authors. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package osd import ( "context" "encoding/json" "fmt" "strconv" "strings" "testing" "time" "github.com/coreos/pkg/capnslog" "github.com/pkg/errors" cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1" "github.com/rook/rook/pkg/client/clientset/versioned/scheme" "github.com/rook/rook/pkg/clusterd" cephclient "github.com/rook/rook/pkg/daemon/ceph/client" cephclientfake "github.com/rook/rook/pkg/daemon/ceph/client/fake" cephver "github.com/rook/rook/pkg/operator/ceph/version" "github.com/rook/rook/pkg/operator/k8sutil" "github.com/rook/rook/pkg/operator/test" exectest "github.com/rook/rook/pkg/util/exec/test" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" k8stesting "k8s.io/client-go/testing" clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" ) var ( // global to allow creating helper functions that are not inline with test functions testIDGenerator osdIDGenerator ) // TODO: look into: failed to calculate diff between current deployment and newly generated one. // Failed to generate strategic merge patch: map: map[] does not contain declared merge key: uid // appears in unit tests, but does it appear in real-world? // Test integration between OSD creates and updates including on both nodes and PVCs. // The definition for this test is a wrapper for the test function that adds a timeout. func TestOSDIntegration(t *testing.T) { oldLogger := *logger defer func() { logger = &oldLogger }() // reset logger to default after this test logger.SetLevel(capnslog.TRACE) // want more log info for this test if it fails oldOpportunisticDuration := osdOpportunisticUpdateDuration oldMinuteDuration := minuteTickerDuration defer func() { osdOpportunisticUpdateDuration = oldOpportunisticDuration minuteTickerDuration = oldMinuteDuration }() // lower the check durations for unit tests to speed them up osdOpportunisticUpdateDuration = 1 * time.Millisecond minuteTickerDuration = 3 * time.Millisecond done := make(chan bool) // runs in less than 650ms on 6-core, 16MB RAM system, but github CI can be much slower timeout := time.After(20 * 750 * time.Millisecond) go func() { // use defer because t.Fatal will kill this goroutine, and we always want done set if the // test func stops running defer func() { done <- true }() // run the actual test testOSDIntegration(t) }() select { case <-timeout: t.Fatal("Test timed out. This is a test failure.") case <-done: } } // This is the actual test. If it hangs, we should consider that an error. func testOSDIntegration(t *testing.T) { ctx := context.TODO() contextCancel, cancel := context.WithCancel(ctx) namespace := "osd-integration" clusterName := "my-cluster" testIDGenerator = newOSDIDGenerator() // mock/stub functions as needed oldConditionExportFunc := updateConditionFunc defer func() { updateConditionFunc = oldConditionExportFunc }() // stub out the conditionExportFunc to do nothing. we do not have a fake Rook interface that // allows us to interact with a CephCluster resource like the fake K8s clientset. updateConditionFunc = func(ctx context.Context, c *clusterd.Context, namespaceName types.NamespacedName, observedGeneration int64, conditionType cephv1.ConditionType, status corev1.ConditionStatus, reason cephv1.ConditionReason, message string) { // do nothing } // set up a fake k8s client set and watcher to generate events that the operator will listen to clientset := test.NewComplexClientset(t) test.AddSomeReadyNodes(t, clientset, 3) assignPodToNode := true test.PrependComplexJobReactor(t, clientset, assignPodToNode) test.SetFakeKubernetesVersion(clientset, "v1.13.2") // v1.13 or higher is required for OSDs on PVC t.Setenv(k8sutil.PodNamespaceEnvVar, namespace) statusMapWatcher := watch.NewRaceFreeFake() clientset.PrependWatchReactor("configmaps", k8stesting.DefaultWatchReactor(statusMapWatcher, nil)) failCreatingDeployments := []string{} failUpdatingDeployments := []string{} deploymentGeneration := int64(1) // mock deployment generation constantly increasing deploymentsCreated := []string{} deploymentsUpdated := []string{} var deploymentReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { switch action := action.(type) { case k8stesting.CreateActionImpl: obj := action.GetObject() d, ok := obj.(*appsv1.Deployment) if !ok { panic(fmt.Sprintf("object not a deployment: %+v", obj)) } t.Logf("deployment reactor: create event for deployment %q", d.Name) // (1) keep track of deployments which have been created deploymentsCreated = append(deploymentsCreated, d.Name) // (2) set deployments ready immediately. Don't have to test waiting for deployments to // be ready b/c that is tested thoroughly in update_test.go d.Status.ObservedGeneration = deploymentGeneration d.Status.UpdatedReplicas = 1 d.Status.ReadyReplicas = 1 deploymentGeneration++ // (3) return a failure if asked for _, match := range failCreatingDeployments { if strings.Contains(d.Name, match) { return true, nil, errors.Errorf("induced error creating deployment %q", d.Name) } } case k8stesting.UpdateActionImpl: obj := action.GetObject() d, ok := obj.(*appsv1.Deployment) if !ok { panic(fmt.Sprintf("object not a deployment: %+v", obj)) } t.Logf("deployment reactor: update event for deployment %q", d.Name) // (1) keep track of deployments which have been created deploymentsUpdated = append(deploymentsUpdated, d.Name) // (2) set deployments ready immediately. Don't have to test waiting for deployments to // be ready b/c that is tested thoroughly in update_test.go d.Status.ObservedGeneration = deploymentGeneration d.Status.UpdatedReplicas = 1 d.Status.ReadyReplicas = 1 deploymentGeneration++ // (3) return a failure if asked for _, match := range failUpdatingDeployments { if strings.Contains(d.Name, match) { return true, nil, errors.Errorf("induced error creating deployment %q", d.Name) } } case k8stesting.DeleteActionImpl: panic(fmt.Sprintf("deployments should not be deleted: %+v", action)) } // modify the object in-place so that the default reactor will create it with our // modifications, if we have made any return false, nil, nil } clientset.PrependReactor("*", "deployments", deploymentReactor) clusterInfo := cephclient.NewClusterInfo(namespace, clusterName) clusterInfo.CephVersion = cephver.Reef clusterInfo.SetName("mycluster") clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t) clusterInfo.Context = ctx executor := osdIntegrationTestExecutor(t, clientset, namespace) cephCluster := &cephv1.CephCluster{ ObjectMeta: metav1.ObjectMeta{ Name: clusterName, Namespace: namespace, }, Spec: cephv1.ClusterSpec{}, } // Objects to track in the fake client. object := []runtime.Object{ cephCluster, } s := scheme.Scheme // Create a fake client to mock API calls. client := clientfake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(object...).Build() rootCtx := &clusterd.Context{ Clientset: clientset, ConfigDir: "/var/lib/rook", Executor: executor, Client: client, } cephCluster.Spec = cephv1.ClusterSpec{ CephVersion: cephv1.CephVersionSpec{ Image: "quay.io/ceph/ceph:v16.2.0", }, DataDirHostPath: rootCtx.ConfigDir, // This storage spec should... (see inline) Storage: cephv1.StorageScopeSpec{ UseAllNodes: true, Selection: cephv1.Selection{ // ... create 2 osd on each of the 3 nodes (6 total) DeviceFilter: "vd[ab]", }, StorageClassDeviceSets: []cephv1.StorageClassDeviceSet{ // ... create 6 portable osds newDummyStorageClassDeviceSet("portable-set", namespace, 6, true, "ec2"), // ... create 3 local, non-portable osds, one on each node newDummyStorageClassDeviceSet("local-set", namespace, 3, false, "local"), }, }, } osdsPerNode := 2 // vda and vdb c := New(rootCtx, clusterInfo, cephCluster.Spec, "myversion") var startErr error var done bool runReconcile := func(ctx context.Context) { // reset environment c = New(rootCtx, clusterInfo, cephCluster.Spec, "myversion") clusterInfo.Context = ctx statusMapWatcher.Reset() // reset counters deploymentsCreated = []string{} deploymentsUpdated = []string{} done = false startErr = c.Start() done = true } waitForDone := func() { for { if done == true { return } time.Sleep(1 * time.Millisecond) } } // NOTE: these tests all use the same environment t.Run("initial creation", func(t *testing.T) { go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 12) // 3 nodes + 9 new PVCs for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 15) assert.Len(t, deploymentsUpdated, 0) }) t.Run("reconcile again with no changes", func(t *testing.T) { go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 0) assert.Len(t, deploymentsUpdated, 15) }) t.Run("increase number of OSDs", func(t *testing.T) { cephCluster.Spec.Storage.Selection.DeviceFilter = "/dev/vd[abc]" // 3 more (1 more per node) cephCluster.Spec.Storage.StorageClassDeviceSets[0].Count = 8 // 2 more portable cephCluster.Spec.Storage.StorageClassDeviceSets[1].Count = 6 // 3 more (1 more per node) osdsPerNode = 3 // vda, vdb, vdc go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 8) // 3 nodes + 5 new PVCs for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 8) assert.Len(t, deploymentsUpdated, 15) }) t.Run("mixed create and update, cancel reconcile, and continue reconcile", func(t *testing.T) { cephCluster.Spec.Storage.Selection.DeviceFilter = "/dev/vd[abcd]" // 3 more (1 more per node) cephCluster.Spec.Storage.StorageClassDeviceSets[0].Count = 10 // 2 more portable osdsPerNode = 4 // vd[a-d] go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 5) // 3 nodes + 2 new PVCs i := 1 for _, cm := range cms { if !strings.Contains(cm.Name, "node") { // only do node configmaps right now since those are always created and we want // a deterministic number of configmaps in the next step continue } cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) if i == 2 { t.Log("canceling orchestration") time.Sleep(10 * time.Millisecond) // after the second status map is made ready, cancel the orchestration. wait a short // while to make sure the watcher picks up the updated change cancel() // refresh the context so the rest of the test can continue contextCancel = context.TODO() break } i++ } waitForDone() assert.Error(t, startErr) t.Logf("c.Start() error: %+v", startErr) // should have created 2 more OSDs for the configmaps we updated assert.Len(t, deploymentsCreated, 2) // we don't know exactly how many updates might have happened by this point numUpdates := len(deploymentsUpdated) t.Logf("deployments updated: %d", numUpdates) go runReconcile(contextCancel) cms = waitForNumConfigMaps(clientset, namespace, 5) // 3 nodes + 2 new PVCs for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 3) // 5 less the 2 created in the previous step assert.Len(t, deploymentsUpdated, 25) // 23 + 2 created in previous step }) t.Run("failures reported in status configmaps", func(t *testing.T) { cephCluster.Spec.Storage.Selection.DeviceFilter = "/dev/vd[abcde]" // 3 more (1 more per node) osdsPerNode = 5 // vd[a-e] go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() if strings.Contains(cm.Name, "node1") { setStatusConfigMapToCompleted(t, cpy, osdsPerNode) } else { setStatusConfigMapToFailed(t, cpy) // fail on node0 and node2 } updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.Error(t, startErr) t.Logf("c.Start() error: %+v", startErr) assert.Len(t, deploymentsCreated, 1) assert.Len(t, deploymentsUpdated, 28) // should get back to healthy after go runReconcile(contextCancel) cms = waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 2) assert.Len(t, deploymentsUpdated, 29) // 28 + 1 created in previous step }) t.Run("failures during deployment updates", func(t *testing.T) { failUpdatingDeployments = []string{"osd-15", "osd-22"} go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.Error(t, startErr) t.Logf("c.Start() error: %+v", startErr) assert.Len(t, deploymentsCreated, 0) assert.Len(t, deploymentsUpdated, 31) // should attempt to update all deployments failUpdatingDeployments = []string{} go runReconcile(contextCancel) cms = waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 0) assert.Len(t, deploymentsUpdated, 31) // all deployments should be updated again }) t.Run("failures during deployment creation", func(t *testing.T) { cephCluster.Spec.Storage.Selection.DeviceFilter = "/dev/vd[abcdef]" // 3 more (1 more per node) osdsPerNode = 6 // vd[a-f] failCreatingDeployments = []string{"osd-31", "osd-33"} go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.Error(t, startErr) t.Logf("c.Start() error: %+v", startErr) assert.Len(t, deploymentsCreated, 3) assert.Len(t, deploymentsUpdated, 31) failCreatingDeployments = []string{} go runReconcile(contextCancel) cms = waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 2) // the 2 deployments NOT created before should be created now assert.Len(t, deploymentsUpdated, 32) // 31 + 1 from previous step }) t.Run("failures from improperly formatted StorageClassDeviceSet", func(t *testing.T) { newSCDS := cephv1.StorageClassDeviceSet{ Name: "new", Count: 3, Portable: true, VolumeClaimTemplates: []cephv1.VolumeClaimTemplate{}, } cephCluster.Spec.Storage.StorageClassDeviceSets = append(cephCluster.Spec.Storage.StorageClassDeviceSets, newSCDS) go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.Error(t, startErr) t.Logf("c.Start() error: %+v", startErr) assert.Len(t, deploymentsCreated, 0) assert.Len(t, deploymentsUpdated, 34) cephCluster.Spec.Storage.StorageClassDeviceSets[2].VolumeClaimTemplates = []cephv1.VolumeClaimTemplate{ newDummyPVC("data", namespace, "100Gi", "ec2"), newDummyPVC("metadata", namespace, "10Gi", "uncle-rogers-secret-stuff"), } go runReconcile(contextCancel) cms = waitForNumConfigMaps(clientset, namespace, 6) // 3 nodes + 3 new PVCs for _, cm := range cms { cpy := cm.DeepCopy() setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, startErr) assert.Len(t, deploymentsCreated, 3) assert.Len(t, deploymentsUpdated, 34) }) t.Run("clean up dangling configmaps", func(t *testing.T) { danglingCM := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "dangling-status-configmap", Namespace: namespace, Labels: statusConfigMapLabels("node0"), }, } _, err := clientset.CoreV1().ConfigMaps(namespace).Create(ctx, danglingCM, metav1.CreateOptions{}) assert.NoError(t, err) go runReconcile(contextCancel) cms := waitForNumConfigMaps(clientset, namespace, 4) // 3 nodes + dangling for _, cm := range cms { cpy := cm.DeepCopy() if cpy.Name == "dangling-status-configmap" { continue } setStatusConfigMapToCompleted(t, cpy, osdsPerNode) updateStatusConfigmap(clientset, statusMapWatcher, cpy) } waitForDone() assert.NoError(t, err) assert.Len(t, deploymentsCreated, 0) assert.Len(t, deploymentsUpdated, 37) cmList, err := clientset.CoreV1().ConfigMaps(namespace).List(ctx, metav1.ListOptions{}) assert.NoError(t, err) assert.Len(t, cmList.Items, 0) }) } /* * mock executor to handle ceph commands */ func osdIntegrationTestExecutor(t *testing.T, clientset *fake.Clientset, namespace string) *exectest.MockExecutor { return &exectest.MockExecutor{ MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) { t.Logf("command: %s %v", command, args) if command != "ceph" { return "", errors.Errorf("unexpected command %q with args %v", command, args) } if args[0] == "auth" { if args[1] == "get-or-create-key" { return "{\"key\": \"does-not-matter\"}", nil } } if args[0] == "osd" { if args[1] == "ok-to-stop" { osdID := args[2] id, err := strconv.Atoi(osdID) if err != nil { panic(err) } t.Logf("returning ok for OSD %d", id) return cephclientfake.OsdOkToStopOutput(id, []int{id}), nil } if args[1] == "ls" { // ceph osd ls returns an array of osd IDs like [0,1,2] // build this based on the number of deployments since they should be equal // for this test l, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { panic(fmt.Sprintf("failed to build 'ceph osd ls' output. %v", err)) } return cephclientfake.OsdLsOutput(len(l.Items)), nil } if args[1] == "tree" { return cephclientfake.OsdTreeOutput(3, 3), nil // fake output for cluster with 3 nodes having 3 OSDs } if args[1] == "crush" { if args[2] == "get-device-class" { return cephclientfake.OSDDeviceClassOutput(args[3]), nil } if args[2] == "class" && args[3] == "ls" { // Mock executor for OSD crush class list command, returning ssd as available device class return `["ssd"]`, nil } } } if args[0] == "versions" { // the update deploy code only cares about the mons from the ceph version command results v := `{"mon":{"ceph version 17.2.1 (somehash) quincy (stable)":3}}` return v, nil } return "", errors.Errorf("unexpected ceph command %q", args) }, } } /* * Unique and consistent OSD ID generator */ type osdIDGenerator struct { nextOSDID int osdIDMap map[string]int } func newOSDIDGenerator() osdIDGenerator { return osdIDGenerator{ nextOSDID: 0, osdIDMap: map[string]int{}, } } func (g *osdIDGenerator) osdID(t *testing.T, namedResource string) int { if id, ok := g.osdIDMap[namedResource]; ok { t.Logf("resource %q has existing OSD ID %d", namedResource, id) return id } id := g.nextOSDID g.osdIDMap[namedResource] = id g.nextOSDID++ t.Logf("generated new OSD ID %d for resource %q", id, namedResource) return id } func newDummyStorageClassDeviceSet( name string, namespace string, count int, portable bool, storageClassName string, ) cephv1.StorageClassDeviceSet { return cephv1.StorageClassDeviceSet{ Name: name, Count: count, Portable: portable, VolumeClaimTemplates: []cephv1.VolumeClaimTemplate{ newDummyPVC("data", namespace, "10Gi", storageClassName), }, } } func waitForNumConfigMaps(clientset kubernetes.Interface, namespace string, count int) []corev1.ConfigMap { for { cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { panic(err) } if len(cms.Items) >= count { return cms.Items } time.Sleep(1 * time.Microsecond) } } // Helper method to set "completed" status on "starting" ConfigMaps. // If the configmap is a result for a provision on nodes, create numOSDsIfOnNode OSDs in the return func setStatusConfigMapToCompleted(t *testing.T, cm *corev1.ConfigMap, numOSDsIfOnNode int) { status := parseOrchestrationStatus(cm.Data) t.Logf("updating configmap %q status to completed", cm.Name) // configmap names are deterministic can be mapped indirectly to an OSD ID, and since the // configmaps are used to report completion status of OSD provisioning, we use this property in // these unit tests status.Status = OrchestrationStatusCompleted if status.PvcBackedOSD { // only one OSD per PVC osdID := testIDGenerator.osdID(t, cm.Name) status.OSDs = []OSDInfo{ { ID: osdID, UUID: fmt.Sprintf("%032d", osdID), BlockPath: "/dev/path/to/block", CVMode: "raw", }, } } else { status.OSDs = []OSDInfo{} for i := 0; i < numOSDsIfOnNode; i++ { // in order to generate multiple OSDs on a node, pretend they have different configmap // names (simply append the index). this is still deterministic. osdID := testIDGenerator.osdID(t, fmt.Sprintf("%s-%d", cm.Name, i)) disk := k8sutil.IndexToName(i) status.OSDs = append(status.OSDs, OSDInfo{ ID: osdID, UUID: fmt.Sprintf("%032d", osdID), BlockPath: fmt.Sprintf("/dev/vd%s", disk), CVMode: "raw", }) } } s, _ := json.Marshal(status) cm.Data[orchestrationStatusKey] = string(s) } func setStatusConfigMapToFailed(t *testing.T, cm *corev1.ConfigMap) { status := parseOrchestrationStatus(cm.Data) t.Logf("updating configmap %q status to failed", cm.Name) status.Status = OrchestrationStatusFailed s, _ := json.Marshal(status) cm.Data[orchestrationStatusKey] = string(s) } func updateStatusConfigmap(clientset kubernetes.Interface, statusMapWatcher *watch.RaceFreeFakeWatcher, cm *corev1.ConfigMap) { _, err := clientset.CoreV1().ConfigMaps(cm.Namespace).Update(context.TODO(), cm, metav1.UpdateOptions{}) if err != nil { panic(err) } statusMapWatcher.Modify(cm) }