Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add v1a2 webconsole request controllers #218

Merged
merged 1 commit into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,26 @@ rules:
- get
- patch
- update
- apiGroups:
- vmoperator.vmware.com
resources:
- virtualmachinewebconsolerequests
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- vmoperator.vmware.com
resources:
- virtualmachinewebconsolerequests/status
verbs:
- get
- patch
- update
- apiGroups:
- vmoperator.vmware.com
resources:
Expand Down
7 changes: 7 additions & 0 deletions controllers/virtualmachinewebconsolerequest/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ 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() {
if err := v1alpha2.AddToManager(ctx, mgr); err != nil {
return err
}
}
return v1alpha1.AddToManager(ctx, mgr)
sreyasn marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading