Skip to content

Commit 04e5ca3

Browse files
authored
Update vm idler (codeready-toolchain#647)
1 parent c185f78 commit 04e5ca3

File tree

4 files changed

+82
-29
lines changed

4 files changed

+82
-29
lines changed

controllers/idler/idler_controller.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"k8s.io/apimachinery/pkg/runtime"
1414
"k8s.io/apimachinery/pkg/runtime/schema"
1515
"k8s.io/client-go/dynamic"
16+
"k8s.io/client-go/rest"
1617
"k8s.io/client-go/scale"
1718
"sigs.k8s.io/controller-runtime/pkg/builder"
1819
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -38,7 +39,8 @@ import (
3839
)
3940

4041
const (
41-
restartThreshold = 50
42+
restartThreshold = 50
43+
vmSubresourceURLFmt = "/apis/subresources.kubevirt.io/%s"
4244
)
4345

4446
var SupportedScaleResources = map[schema.GroupVersionKind]schema.GroupVersionResource{
@@ -63,6 +65,7 @@ type Reconciler struct {
6365
Client client.Client
6466
Scheme *runtime.Scheme
6567
AllNamespacesClient client.Client
68+
RestClient rest.Interface
6669
ScalesClient scale.ScalesGetter
6770
DynamicClient dynamic.Interface
6871
GetHostCluster cluster.GetHostClusterFunc
@@ -79,6 +82,10 @@ type Reconciler struct {
7982
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
8083
//+kubebuilder:rbac:groups=kubevirt.io,resources=virtualmachines;virtualmachineinstances,verbs=get;list;watch;create;update;patch;delete
8184

85+
// needed to stop the VMs - we need to make a PUT request for the "stop" subresource. Kubernetes internally classifies these as either create or update
86+
// based on the state of the existing object.
87+
//+kubebuilder:rbac:groups=subresources.kubevirt.io,resources=virtualmachines/stop,verbs=create;update
88+
8289
// Reconcile reads that state of the cluster for an Idler object and makes changes based on the state read
8390
// and what is in the Idler.Spec
8491
// Note:
@@ -338,7 +345,7 @@ func (r *Reconciler) getUserEmailsFromMURs(ctx context.Context, hostCluster *clu
338345

339346
// scaleControllerToZero checks if the object has an owner controller (Deployment, ReplicaSet, etc)
340347
// and scales the owner down to zero and returns "true".
341-
// Otherwise returns "false".
348+
// Otherwise, returns "false".
342349
// It also returns the parent controller type and name or empty strings if there is no parent controller.
343350
func (r *Reconciler) scaleControllerToZero(ctx context.Context, meta metav1.ObjectMeta) (string, string, bool, error) {
344351
log.FromContext(ctx).Info("Scaling controller to zero", "name", meta.Name)
@@ -609,9 +616,14 @@ func (r *Reconciler) stopVirtualMachine(ctx context.Context, namespace string, o
609616
return "", "", false, err
610617
}
611618

612-
// patch the virtualmachine resource by setting spec.running to false in order to stop the VM
613-
patch := []byte(`{"spec":{"running":false}}`)
614-
_, err = r.DynamicClient.Resource(vmGVR).Namespace(namespace).Patch(ctx, vm.GetName(), types.MergePatchType, patch, metav1.PatchOptions{})
619+
err = r.RestClient.Put().
620+
AbsPath(fmt.Sprintf(vmSubresourceURLFmt, "v1")).
621+
Namespace(vm.GetNamespace()).
622+
Resource("virtualmachines").
623+
Name(vm.GetName()).
624+
SubResource("stop").
625+
Do(ctx).
626+
Error()
615627
if err != nil {
616628
return "", "", false, err
617629
}

controllers/idler/idler_controller_test.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/codeready-toolchain/toolchain-common/pkg/cluster"
1717
"github.com/codeready-toolchain/toolchain-common/pkg/condition"
1818
"github.com/codeready-toolchain/toolchain-common/pkg/test"
19+
"gopkg.in/h2non/gock.v1"
1920
"k8s.io/apimachinery/pkg/types"
2021
"sigs.k8s.io/controller-runtime/pkg/log/zap"
2122

@@ -46,6 +47,7 @@ const (
4647
RestartCountWithinThresholdContainer2 = 24
4748
RestartCountOverThreshold = 52
4849
TestIdlerTimeOutSeconds = 540
50+
apiEndpoint = "https://api.openshift.com:6443"
4951
)
5052

5153
func TestReconcile(t *testing.T) {
@@ -217,9 +219,9 @@ func TestEnsureIdling(t *testing.T) {
217219
StatefulSetScaledUp(podsTooEarlyToKill.statefulSet).
218220
StatefulSetScaledUp(noise.statefulSet).
219221
StatefulSetScaledUp(podsCrashLoopingWithinThreshold.statefulSet).
220-
VMRunning(podsRunningForTooLong.virtualmachine).
221-
VMRunning(podsTooEarlyToKill.virtualmachine).
222-
VMRunning(noise.virtualmachine)
222+
VMRunning(podsRunningForTooLong.vmStopCallCounter).
223+
VMRunning(podsTooEarlyToKill.vmStopCallCounter).
224+
VMRunning(noise.vmStopCallCounter)
223225

224226
// after golang 1.22 upgrade can use slices.Concat(podsTooEarlyToKill.allPods, podsRunningForTooLong.allPods, podsCrashLooping.allPods, podsCrashLoopingWithinThreshold.allPods)
225227
// Tracked pods
@@ -273,9 +275,9 @@ func TestEnsureIdling(t *testing.T) {
273275
StatefulSetScaledUp(podsTooEarlyToKill.statefulSet).
274276
StatefulSetScaledUp(noise.statefulSet).
275277
StatefulSetScaledUp(podsCrashLoopingWithinThreshold.statefulSet).
276-
VMStopped(podsRunningForTooLong.virtualmachine).
277-
VMRunning(podsTooEarlyToKill.virtualmachine).
278-
VMRunning(noise.virtualmachine)
278+
VMStopped(podsRunningForTooLong.vmStopCallCounter).
279+
VMRunning(podsTooEarlyToKill.vmStopCallCounter).
280+
VMRunning(noise.vmStopCallCounter)
279281

280282
// Only tracks pods that have not been deleted
281283
memberoperatortest.AssertThatIdler(t, idler.Name, cl).
@@ -624,6 +626,9 @@ func TestEnsureIdlingFailed(t *testing.T) {
624626
assertCanNotUpdateObject := func(inaccessible runtime.Object, errMsg string) {
625627
// given
626628
reconciler, req, cl, allCl, dynamicCl := prepareReconcileWithPodsRunningTooLong(t, idler)
629+
gock.Off()
630+
// mock stop call
631+
mockStopVMCalls(".*", ".*", http.StatusInternalServerError)
627632

628633
update := allCl.MockUpdate
629634
defer func() { allCl.MockUpdate = update }()
@@ -661,7 +666,7 @@ func TestEnsureIdlingFailed(t *testing.T) {
661666
assertCanNotUpdateObject(&appsv1.StatefulSet{}, "can't update statefulset")
662667
assertCanNotUpdateObject(&openshiftappsv1.DeploymentConfig{}, "can't update deploymentconfig")
663668
assertCanNotUpdateObject(&corev1.ReplicationController{}, "can't update replicationcontroller")
664-
assertCanNotUpdateObject(vm, "can't patch virtualmachine")
669+
assertCanNotUpdateObject(vm, "an error on the server (\"\") has prevented the request from succeeding (put virtualmachines.authentication.k8s.io alex-stage-virtualmachine)")
665670
})
666671

667672
t.Run("can't delete payloads", func(t *testing.T) {
@@ -1173,6 +1178,7 @@ type payloads struct {
11731178
replicationController *corev1.ReplicationController
11741179
job *batchv1.Job
11751180
virtualmachine *unstructured.Unstructured
1181+
vmStopCallCounter *int
11761182
virtualmachineinstance *unstructured.Unstructured
11771183
}
11781184

@@ -1319,6 +1325,9 @@ func preparePayloads(t *testing.T, r *Reconciler, namespace, namePrefix string,
13191325
_, err = r.DynamicClient.Resource(vmGVR).Namespace(namespace).Create(context.TODO(), vm, metav1.CreateOptions{})
13201326
require.NoError(t, err)
13211327

1328+
// mock stop call
1329+
stopCallCounter := mockStopVMCalls(namespace, vm.GetName(), http.StatusAccepted)
1330+
13221331
// VirtualMachineInstance
13231332
vmstartTime := metav1.NewTime(startTimes.vmStartTime)
13241333
vmi := &unstructured.Unstructured{}
@@ -1379,10 +1388,30 @@ func preparePayloads(t *testing.T, r *Reconciler, namespace, namePrefix string,
13791388
replicationController: standaloneRC,
13801389
job: job,
13811390
virtualmachine: vm,
1391+
vmStopCallCounter: stopCallCounter,
13821392
virtualmachineinstance: vmi,
13831393
}
13841394
}
13851395

1396+
func mockStopVMCalls(namespace, name string, reply int) *int {
1397+
expPath := fmt.Sprintf("/apis/subresources.kubevirt.io/v1/namespaces/%s/virtualmachines/%s/stop", namespace, name)
1398+
stopCallCounter := new(int)
1399+
gock.New(apiEndpoint).
1400+
Put(expPath).
1401+
Persist().
1402+
AddMatcher(func(request *http.Request, request2 *gock.Request) (bool, error) {
1403+
// the matcher function is called before checking the path,
1404+
// so we need to verify that it's really the same VM
1405+
if request.URL.Path == expPath {
1406+
*stopCallCounter++
1407+
}
1408+
return true, nil
1409+
}).
1410+
Reply(reply).
1411+
BodyString("")
1412+
return stopCallCounter
1413+
}
1414+
13861415
func preparePayloadsSinglePod(t *testing.T, r *Reconciler, namespace, namePrefix string, startTime time.Time, conditions ...corev1.PodCondition) payloads {
13871416
sTime := metav1.NewTime(startTime)
13881417

@@ -1563,10 +1592,18 @@ func prepareReconcile(t *testing.T, name string, getHostClusterFunc func(fakeCli
15631592
return true, obj, nil
15641593
})
15651594

1595+
restClient, err := test.NewRESTClient("dummy-token", apiEndpoint)
1596+
require.NoError(t, err)
1597+
restClient.Client.Transport = gock.DefaultTransport
1598+
t.Cleanup(func() {
1599+
gock.OffAll()
1600+
})
1601+
15661602
r := &Reconciler{
15671603
Client: fakeClient,
15681604
AllNamespacesClient: allNamespacesClient,
15691605
DynamicClient: dynamicClient,
1606+
RestClient: restClient,
15701607
ScalesClient: &scalesClient,
15711608
Scheme: s,
15721609
GetHostCluster: getHostClusterFunc(fakeClient),

main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ func main() {
207207
os.Exit(1)
208208
}
209209

210+
restClient, err := newRestClient(cfg)
211+
if err != nil {
212+
setupLog.Error(err, "unable to create scales client")
213+
os.Exit(1)
214+
}
215+
210216
dynamicClient, err := dynamic.NewForConfig(cfg)
211217
if err != nil {
212218
setupLog.Error(err, "unable to create dynamic client")
@@ -245,6 +251,7 @@ func main() {
245251
Client: mgr.GetClient(),
246252
ScalesClient: scalesClient,
247253
DynamicClient: dynamicClient,
254+
RestClient: restClient,
248255
GetHostCluster: cluster.GetHostCluster,
249256
Namespace: namespace,
250257
}).SetupWithManager(mgr, allNamespacesCluster); err != nil {
@@ -323,6 +330,15 @@ func main() {
323330

324331
}
325332

333+
func newRestClient(config *rest.Config) (*rest.RESTClient, error) {
334+
httpClient, err := rest.HTTPClientFor(config)
335+
if err != nil {
336+
return nil, err
337+
}
338+
339+
return rest.RESTClientForConfigAndClient(config, httpClient)
340+
}
341+
326342
func newScalesClient(config *rest.Config) (scale.ScalesGetter, error) {
327343
c, err := kubernetes.NewForConfig(config)
328344
if err != nil {

test/idler_assertion.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@ import (
1717
corev1 "k8s.io/api/core/v1"
1818
apierrors "k8s.io/apimachinery/pkg/api/errors"
1919
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
21-
"k8s.io/apimachinery/pkg/runtime/schema"
2220
"k8s.io/apimachinery/pkg/types"
2321
fakedynamic "k8s.io/client-go/dynamic/fake"
2422
"sigs.k8s.io/controller-runtime/pkg/client"
2523
)
2624

27-
var vmGVR = schema.GroupVersionResource{Group: "kubevirt.io", Version: "v1", Resource: "virtualmachines"}
28-
2925
type IdlerAssertion struct {
3026
idler *toolchainv1alpha1.Idler
3127
client client.Client
@@ -275,20 +271,12 @@ func (a *IdleablePayloadAssertion) StatefulSetScaledUp(statefulSet *appsv1.State
275271
return a
276272
}
277273

278-
func (a *IdleablePayloadAssertion) VMRunning(vm *unstructured.Unstructured) *IdleablePayloadAssertion {
279-
return a.vmRunning(vm, true)
280-
}
281-
282-
func (a *IdleablePayloadAssertion) VMStopped(vm *unstructured.Unstructured) *IdleablePayloadAssertion {
283-
return a.vmRunning(vm, false)
274+
func (a *IdleablePayloadAssertion) VMRunning(vmStopCallCounter *int) *IdleablePayloadAssertion {
275+
assert.Empty(a.t, *vmStopCallCounter)
276+
return a
284277
}
285278

286-
func (a *IdleablePayloadAssertion) vmRunning(vm *unstructured.Unstructured, running bool) *IdleablePayloadAssertion {
287-
vm, err := a.dynamicClient.Resource(vmGVR).Namespace(vm.GetNamespace()).Get(context.TODO(), vm.GetName(), metav1.GetOptions{})
288-
require.NoError(a.t, err)
289-
val, found, err := unstructured.NestedBool(vm.Object, "spec", "running")
290-
require.NoError(a.t, err)
291-
assert.True(a.t, found)
292-
assert.Equal(a.t, running, val)
279+
func (a *IdleablePayloadAssertion) VMStopped(vmStopCallCounter *int) *IdleablePayloadAssertion {
280+
assert.NotEmpty(a.t, *vmStopCallCounter)
293281
return a
294282
}

0 commit comments

Comments
 (0)