Skip to content

Commit

Permalink
Add v1a2 webconsole request controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
sreyasn committed Sep 8, 2023
1 parent 4c603eb commit 52f1dc3
Show file tree
Hide file tree
Showing 6 changed files with 534 additions and 3 deletions.
5 changes: 5 additions & 0 deletions controllers/virtualmachinewebconsolerequest/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest/v1alpha1"
"github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest/v1alpha2"
"github.com/vmware-tanzu/vm-operator/pkg/context"
"github.com/vmware-tanzu/vm-operator/pkg/lib"
)

// AddToManager adds the controller to the provided manager.
func AddToManager(ctx *context.ControllerManagerContext, mgr manager.Manager) error {
if lib.IsVMServiceV1Alpha2FSSEnabled() {
return v1alpha2.AddToManager(ctx, mgr)
}
return v1alpha1.AddToManager(ctx, mgr)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package v1alpha2

import (
goctx "context"

"fmt"
"reflect"
"strings"
"time"

"github.com/go-logr/logr"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"

vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2"
"github.com/vmware-tanzu/vm-operator/pkg/context"
"github.com/vmware-tanzu/vm-operator/pkg/patch"
"github.com/vmware-tanzu/vm-operator/pkg/record"
"github.com/vmware-tanzu/vm-operator/pkg/vmprovider"
)

const (
DefaultExpiryTime = time.Second * 120
UUIDLabelKey = "vmoperator.vmware.com/webconsolerequest-uuid"

ProxyAddrServiceName = "kube-apiserver-lb-svc"
ProxyAddrServiceNamespace = "kube-system"
)

// AddToManager adds this package's controller to the provided manager.
func AddToManager(ctx *context.ControllerManagerContext, mgr manager.Manager) error {
var (
controlledType = &vmopv1.VirtualMachineWebConsoleRequest{}
controlledTypeName = reflect.TypeOf(controlledType).Elem().Name()

controllerNameShort = fmt.Sprintf("%s-controller", strings.ToLower(controlledTypeName))
controllerNameLong = fmt.Sprintf("%s/%s/%s", ctx.Namespace, ctx.Name, controllerNameShort)
)

r := NewReconciler(
mgr.GetClient(),
ctrl.Log.WithName("controllers").WithName(controlledTypeName),
record.New(mgr.GetEventRecorderFor(controllerNameLong)),
ctx.VMProviderA2,
)

return ctrl.NewControllerManagedBy(mgr).
For(controlledType).
WithOptions(controller.Options{MaxConcurrentReconciles: 1}).
Complete(r)
}

func NewReconciler(
client client.Client,
logger logr.Logger,
recorder record.Recorder,
vmProvider vmprovider.VirtualMachineProviderInterfaceA2) *Reconciler {
return &Reconciler{
Client: client,
Logger: logger,
Recorder: recorder,
VMProvider: vmProvider,
}
}

// Reconciler reconciles a WebConsoleRequest object.
type Reconciler struct {
client.Client
Logger logr.Logger
Recorder record.Recorder
VMProvider vmprovider.VirtualMachineProviderInterfaceA2
}

// -kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachinewebconsolerequests,verbs=get;list;watch;create;update;patch;delete
// -kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachinewebconsolerequests/status,verbs=get;update;patch
// -kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachines,verbs=get;list
// -kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch
// -kubebuilder:rbac:groups="",resources=services/status,verbs=get

func (r *Reconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
webconsolerequest := &vmopv1.VirtualMachineWebConsoleRequest{}
err := r.Get(ctx, req.NamespacedName, webconsolerequest)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

webConsoleRequestCtx := &context.WebConsoleRequestContextA2{
Context: ctx,
Logger: ctrl.Log.WithName("WebConsoleRequest").WithValues("name", req.NamespacedName),
WebConsoleRequest: webconsolerequest,
VM: &vmopv1.VirtualMachine{},
}

done, err := r.ReconcileEarlyNormal(webConsoleRequestCtx)
if err != nil {
webConsoleRequestCtx.Logger.Error(err, "failed to expire WebConsoleRequest")
return ctrl.Result{}, err
}
if done {
return ctrl.Result{}, nil
}

err = r.Get(ctx, client.ObjectKey{Name: webconsolerequest.Spec.Name, Namespace: webconsolerequest.Namespace}, webConsoleRequestCtx.VM)
if err != nil {
r.Recorder.Warn(webConsoleRequestCtx.WebConsoleRequest, "VirtualMachine Not Found", "")
webConsoleRequestCtx.Logger.Error(err, "failed to get subject vm %s", webconsolerequest.Spec.Name)
return ctrl.Result{}, errors.Wrapf(err, "failed to get subject vm %s", webconsolerequest.Spec.Name)
}

patchHelper, err := patch.NewHelper(webconsolerequest, r.Client)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to init patch helper for %s: %w", webConsoleRequestCtx, err)
}
defer func() {
if err := patchHelper.Patch(ctx, webconsolerequest); err != nil {
if reterr == nil {
reterr = err
}
webConsoleRequestCtx.Logger.Error(err, "patch failed")
}
}()

if err := r.ReconcileNormal(webConsoleRequestCtx); err != nil {
webConsoleRequestCtx.Logger.Error(err, "failed to reconcile WebConsoleRequest")
return ctrl.Result{}, err
}

return ctrl.Result{Requeue: true, RequeueAfter: DefaultExpiryTime}, nil
}

func (r *Reconciler) ReconcileEarlyNormal(ctx *context.WebConsoleRequestContextA2) (bool, error) {
expiryTime := ctx.WebConsoleRequest.Status.ExpiryTime
nowTime := metav1.Now()
if !expiryTime.IsZero() && !nowTime.Before(&expiryTime) {
err := r.Delete(ctx, ctx.WebConsoleRequest)
if client.IgnoreNotFound(err) != nil {
return false, errors.Wrapf(err, "failed to delete webconsolerequest")
}
ctx.Logger.Info("Deleted expired WebConsoleRequest")
return true, nil
}

if ctx.WebConsoleRequest.Status.Response != "" &&
ctx.WebConsoleRequest.Status.ProxyAddr != "" {
// If the response and proxy address are already set, no need to reconcile anymore
ctx.Logger.Info("Response and proxy address already set, skip reconciling")
return true, nil
}

return false, nil
}

func (r *Reconciler) ReconcileNormal(ctx *context.WebConsoleRequestContextA2) error {
ctx.Logger.Info("Reconciling WebConsoleRequest")
defer func() {
ctx.Logger.Info("Finished reconciling WebConsoleRequest")
}()

ticket, err := r.VMProvider.GetVirtualMachineWebMKSTicket(ctx, ctx.VM, ctx.WebConsoleRequest.Spec.PublicKey)
if err != nil {
return errors.Wrapf(err, "failed to get webmksticket")
}
r.Recorder.EmitEvent(ctx.WebConsoleRequest, "Acquired Ticket", nil, false)

ctx.WebConsoleRequest.Status.Response = ticket
ctx.WebConsoleRequest.Status.ExpiryTime = metav1.NewTime(metav1.Now().Add(DefaultExpiryTime))

// Retrieve the proxy address from the load balancer service ingress IP.
proxySvc := &corev1.Service{}
proxySvcObjectKey := client.ObjectKey{Name: ProxyAddrServiceName, Namespace: ProxyAddrServiceNamespace}
err = r.Get(ctx, proxySvcObjectKey, proxySvc)
if err != nil {
return errors.Wrapf(err, "failed to get proxy address service %s", proxySvcObjectKey)
}
if len(proxySvc.Status.LoadBalancer.Ingress) == 0 {
return errors.Errorf("no ingress found for proxy address service %s", proxySvcObjectKey)
}

ctx.WebConsoleRequest.Status.ProxyAddr = proxySvc.Status.LoadBalancer.Ingress[0].IP

// Add UUID as a Label to the current WebConsoleRequest resource after acquiring the ticket.
// This will be used when validating the connection request from users to the web console URL.
if ctx.WebConsoleRequest.Labels == nil {
ctx.WebConsoleRequest.Labels = make(map[string]string)
}
ctx.WebConsoleRequest.Labels[UUIDLabelKey] = string(ctx.WebConsoleRequest.UID)

err = r.ReconcileOwnerReferences(ctx)
if err != nil {
return err
}

return nil
}

func (r *Reconciler) ReconcileOwnerReferences(ctx *context.WebConsoleRequestContextA2) error {
isController := true
ownerRef := metav1.OwnerReference{
APIVersion: ctx.VM.APIVersion,
Kind: ctx.VM.Kind,
Name: ctx.VM.Name,
UID: ctx.VM.UID,
Controller: &isController,
}

ctx.WebConsoleRequest.SetOwnerReferences([]metav1.OwnerReference{ownerRef})
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package v1alpha2_test

import (
"context"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2"
webconsolerequest "github.com/vmware-tanzu/vm-operator/controllers/virtualmachinewebconsolerequest/v1alpha1"
"github.com/vmware-tanzu/vm-operator/test/builder"
)

func intgTests() {
Describe("Invoking WebConsoleRequest controller tests", webConsoleRequestReconcile)
}

func webConsoleRequestReconcile() {
var (
ctx *builder.IntegrationTestContext
wcr *vmopv1.VirtualMachineWebConsoleRequest
vm *vmopv1.VirtualMachine
proxySvc *corev1.Service
)

getWebConsoleRequest := func(ctx *builder.IntegrationTestContext, objKey types.NamespacedName) *vmopv1.VirtualMachineWebConsoleRequest {
wcr := &vmopv1.VirtualMachineWebConsoleRequest{}
if err := ctx.Client.Get(ctx, objKey, wcr); err != nil {
return nil
}
return wcr
}

BeforeEach(func() {
ctx = suite.NewIntegrationTestContext()

vm = &vmopv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "dummy-vm",
Namespace: ctx.Namespace,
},
Spec: vmopv1.VirtualMachineSpec{
ImageName: "dummy-image",
PowerState: vmopv1.VirtualMachinePowerStateOn,
},
}

_, publicKeyPem := builder.WebConsoleRequestKeyPair()

wcr = &vmopv1.VirtualMachineWebConsoleRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "dummy-wcr",
Namespace: ctx.Namespace,
},
Spec: vmopv1.VirtualMachineWebConsoleRequestSpec{
Name: vm.Name,
PublicKey: publicKeyPem,
},
}

proxySvc = &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: webconsolerequest.ProxyAddrServiceName,
Namespace: webconsolerequest.ProxyAddrServiceNamespace,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "dummy-proxy-port",
Port: 443,
},
},
},
}

fakeVMProvider.Lock()
defer fakeVMProvider.Unlock()
fakeVMProvider.GetVirtualMachineWebMKSTicketFn = func(ctx context.Context, vm *vmopv1.VirtualMachine, pubKey string) (string, error) {
return "some-fake-webmksticket", nil
}
})

AfterEach(func() {
ctx.AfterEach()
ctx = nil
fakeVMProvider.Reset()
})

Context("Reconcile", func() {
BeforeEach(func() {
Expect(ctx.Client.Create(ctx, vm)).To(Succeed())
Expect(ctx.Client.Create(ctx, wcr)).To(Succeed())
Expect(ctx.Client.Create(ctx, proxySvc)).To(Succeed())
proxySvc.Status = corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
IP: "192.168.0.1",
},
},
},
}
Expect(ctx.Client.Status().Update(ctx, proxySvc)).To(Succeed())
})

AfterEach(func() {
err := ctx.Client.Delete(ctx, wcr)
Expect(err == nil || k8serrors.IsNotFound(err)).To(BeTrue())
err = ctx.Client.Delete(ctx, vm)
Expect(err == nil || k8serrors.IsNotFound(err)).To(BeTrue())
})

It("resource successfully created", func() {
Eventually(func() bool {
wcr = getWebConsoleRequest(ctx, types.NamespacedName{Name: wcr.Name, Namespace: wcr.Namespace})
if wcr != nil && wcr.Status.Response != "" {
return true
}
return false
}).Should(BeTrue(), "waiting for webconsolerequest to be")
Expect(wcr.Status.ProxyAddr).To(Equal("192.168.0.1"))
Expect(wcr.Status.Response).ToNot(BeEmpty())
Expect(wcr.Status.ExpiryTime.Time).To(BeTemporally("~", time.Now(), webconsolerequest.DefaultExpiryTime))
Expect(wcr.Labels).To(HaveKeyWithValue(webconsolerequest.UUIDLabelKey, string(wcr.UID)))
})
})
}
Loading

0 comments on commit 52f1dc3

Please sign in to comment.