-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add v1a2 webconsole request controllers
- Loading branch information
Showing
6 changed files
with
534 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
216 changes: 216 additions & 0 deletions
216
controllers/virtualmachinewebconsolerequest/v1alpha2/webconsolerequest_controller.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
136 changes: 136 additions & 0 deletions
136
controllers/virtualmachinewebconsolerequest/v1alpha2/webconsolerequest_intg_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
}) | ||
}) | ||
} |
Oops, something went wrong.