diff --git a/cmd/liqoctl/cmd/offload.go b/cmd/liqoctl/cmd/offload.go index 41d67c38d1..ac25f4bee7 100644 --- a/cmd/liqoctl/cmd/offload.go +++ b/cmd/liqoctl/cmd/offload.go @@ -128,6 +128,7 @@ func newOffloadNamespaceCommand(ctx context.Context, f *factory.Factory) *cobra. f.Printer.CheckErr(cmd.RegisterFlagCompletionFunc("pod-offloading-strategy", completion.Enumeration(podOffloadingStrategy.Allowed))) f.Printer.CheckErr(cmd.RegisterFlagCompletionFunc("namespace-mapping-strategy", completion.Enumeration(namespaceMappingStrategy.Allowed))) + f.Printer.CheckErr(cmd.RegisterFlagCompletionFunc("selector", completion.LabelsSelector(ctx, f, completion.NoLimit))) f.Printer.CheckErr(cmd.RegisterFlagCompletionFunc("output", completion.Enumeration(outputFormat.Allowed))) return cmd diff --git a/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml b/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml index 4b97a88fcc..b6b4fa0787 100644 --- a/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml +++ b/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml @@ -460,6 +460,16 @@ rules: - get - patch - update +- apiGroups: + - virtualkubelet.liqo.io + resources: + - virtualnode + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - virtualkubelet.liqo.io resources: diff --git a/pkg/liqo-controller-manager/namespaceoffloading-controller/clusterselector.go b/pkg/liqo-controller-manager/namespaceoffloading-controller/clusterselector.go index 7fa2621ae9..d5c3890879 100644 --- a/pkg/liqo-controller-manager/namespaceoffloading-controller/clusterselector.go +++ b/pkg/liqo-controller-manager/namespaceoffloading-controller/clusterselector.go @@ -31,6 +31,7 @@ import ( "github.com/liqotech/liqo/internal/crdReplicator/reflection" liqoconst "github.com/liqotech/liqo/pkg/consts" "github.com/liqotech/liqo/pkg/utils/getters" + virtualnodeutils "github.com/liqotech/liqo/pkg/utils/virtualnode" ) func (r *NamespaceOffloadingReconciler) enforceClusterSelector(ctx context.Context, nsoff *offv1alpha1.NamespaceOffloading, @@ -102,10 +103,10 @@ func matchVirtualNodeSelectorTerms(ctx context.Context, cl client.Client, virtua if len(selector.NodeSelectorTerms) == 0 { return true, nil } - - n, err := getters.GetNodeFromVirtualNode(ctx, cl, virtualNode) + var n *corev1.Node + n, err := virtualnodeutils.ForgeFakeNodeFromVirtualNode(ctx, cl, virtualNode) if err != nil { - return false, fmt.Errorf("failed to retrieve node %s from VirtualNode %s: %w", n.Name, virtualNode.Name, err) + return false, fmt.Errorf("failed to forge fake node from VirtualNode: %w", err) } return k8shelper.MatchNodeSelectorTerms(n, selector) } diff --git a/pkg/liqo-controller-manager/namespaceoffloading-controller/namespaceoffloading_controller.go b/pkg/liqo-controller-manager/namespaceoffloading-controller/namespaceoffloading_controller.go index 2ab062e2bb..31ab20f507 100644 --- a/pkg/liqo-controller-manager/namespaceoffloading-controller/namespaceoffloading_controller.go +++ b/pkg/liqo-controller-manager/namespaceoffloading-controller/namespaceoffloading_controller.go @@ -17,6 +17,7 @@ package nsoffctrl import ( "context" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" @@ -56,6 +57,7 @@ const ( // +kubebuilder:rbac:groups=offloading.liqo.io,resources=namespaceoffloadings/status,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=offloading.liqo.io,resources=namespaceoffloadings/finalizers,verbs=get;update;patch // +kubebuilder:rbac:groups=virtualkubelet.liqo.io,resources=namespacemaps,verbs=get;list;watch;patch;update +// +kubebuilder:rbac:groups=virtualkubelet.liqo.io,resources=virtualnode, verbs=get;list;watch;patch;update // +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete @@ -133,6 +135,8 @@ func (r *NamespaceOffloadingReconciler) SetupWithManager(mgr ctrl.Manager) error return ctrl.NewControllerManagedBy(mgr). For(&offv1alpha1.NamespaceOffloading{}, builder.WithPredicates(filter)). Watches(&mapsv1alpha1.NamespaceMap{}, r.namespaceMapHandlers()). + Watches(&mapsv1alpha1.VirtualNode{}, r.enqueueAll()). + Watches(&corev1.Node{}, r.enqueueAll()). Complete(r) } @@ -173,3 +177,22 @@ func (r *NamespaceOffloadingReconciler) namespaceMapHandlers() handler.EventHand }, } } + +func (r *NamespaceOffloadingReconciler) enqueueAll() handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + var nsolist offv1alpha1.NamespaceOffloadingList + if err := r.Client.List(ctx, &nsolist); err != nil { + klog.Errorf("Failed to retrieve NamespaceOffloadingList: %v", err) + return nil + } + reqs := make([]reconcile.Request, len(nsolist.Items)) + for i := range nsolist.Items { + reqs[i] = reconcile.Request{NamespacedName: types.NamespacedName{ + Name: nsolist.Items[i].Name, + Namespace: nsolist.Items[i].Namespace, + }} + } + return reqs + }, + ) +} diff --git a/pkg/liqoctl/completion/completion.go b/pkg/liqoctl/completion/completion.go index 6eae80446f..c4bc37be6a 100644 --- a/pkg/liqoctl/completion/completion.go +++ b/pkg/liqoctl/completion/completion.go @@ -16,6 +16,7 @@ package completion import ( "context" + "fmt" "strings" "github.com/spf13/cobra" @@ -28,6 +29,7 @@ import ( identitymanager "github.com/liqotech/liqo/pkg/identityManager" "github.com/liqotech/liqo/pkg/liqoctl/factory" "github.com/liqotech/liqo/pkg/utils/slice" + utilsvirtualnode "github.com/liqotech/liqo/pkg/utils/virtualnode" ) // NoLimit is a constant to specify that autocompletion is not limited depending on the number of arguments. @@ -143,6 +145,49 @@ func VirtualNodes(ctx context.Context, f *factory.Factory, argsLimit int) FnType return common(ctx, f, argsLimit, retriever) } +// LabelsSelector returns a function to autocomplete selector labels. +func LabelsSelector(ctx context.Context, f *factory.Factory, argsLimit int) FnType { + retriever := func(ctx context.Context, f *factory.Factory) ([]string, error) { + // labelsCounter contains a 'key=value' string as key and the number of times it appears as value. + labelsCounter := map[string]int{} + var virtualNodes virtualkubeletv1alpha1.VirtualNodeList + if err := f.CRClient.List(ctx, &virtualNodes); err != nil { + return nil, err + } + for i := range virtualNodes.Items { + labelSet, err := utilsvirtualnode.GetLabelSelectors(ctx, f.CRClient, &virtualNodes.Items[i]) + if err != nil { + return nil, err + } + for k, v := range labelSet { + addLabelSelector(k, v, labelsCounter) + } + } + return parseLabelSelectors(labelsCounter, len(virtualNodes.Items)), nil + } + return common(ctx, f, argsLimit, retriever) +} + +func addLabelSelector(key, value string, labelset map[string]int) { + entry := fmt.Sprintf("%s=%s", key, value) + if _, ok := labelset[entry]; ok { + labelset[entry]++ + return + } + labelset[entry] = 1 +} + +func parseLabelSelectors(labelset map[string]int, max int) []string { + var output []string + for k, v := range labelset { + if v != max { + // this means that the label is not present in all virtualnodes or node, so can be used as selector + output = append(output, k) + } + } + return output +} + // ForeignClusters returns a function to autocomplete ForeignCluster names. func ForeignClusters(ctx context.Context, f *factory.Factory, argsLimit int) FnType { retriever := func(ctx context.Context, f *factory.Factory) ([]string, error) { diff --git a/pkg/utils/getters/k8sGetters.go b/pkg/utils/getters/k8sGetters.go index 16bc2f3dfc..9ee21333ec 100644 --- a/pkg/utils/getters/k8sGetters.go +++ b/pkg/utils/getters/k8sGetters.go @@ -271,9 +271,9 @@ func GetNodeFromVirtualNode(ctx context.Context, cl client.Client, virtualNode * return &nodes.Items[i], nil } } - podRN := string(corev1.ResourcePods) - podGR := corev1.Resource(podRN) - return nil, kerrors.NewNotFound(podGR, nodename) + nodeRN := "nodes" + nodeGR := corev1.Resource(nodeRN) + return nil, kerrors.NewNotFound(nodeGR, nodename) } // GetTunnelEndpoint retrieves the tunnelEndpoint resource related to a cluster. diff --git a/pkg/utils/virtualnode/doc.go b/pkg/utils/virtualnode/doc.go new file mode 100644 index 0000000000..d10606c655 --- /dev/null +++ b/pkg/utils/virtualnode/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019-2023 The Liqo Authors +// +// 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 virtualnode contains utils functions to manage virtual nodes. +package virtualnode diff --git a/pkg/utils/virtualnode/virtualnode.go b/pkg/utils/virtualnode/virtualnode.go new file mode 100644 index 0000000000..c0a19d554b --- /dev/null +++ b/pkg/utils/virtualnode/virtualnode.go @@ -0,0 +1,63 @@ +// Copyright 2019-2023 The Liqo Authors +// +// 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 virtualnode + +import ( + "context" + "fmt" + "strconv" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + + virtualkubeletv1alpha1 "github.com/liqotech/liqo/apis/virtualkubelet/v1alpha1" + liqoconsts "github.com/liqotech/liqo/pkg/consts" + "github.com/liqotech/liqo/pkg/utils/getters" +) + +// ForgeFakeNodeFromVirtualNode creates a fake node from a virtual node. +func ForgeFakeNodeFromVirtualNode(ctx context.Context, cl client.Client, vn *virtualkubeletv1alpha1.VirtualNode) (*corev1.Node, error) { + l, err := GetLabelSelectors(ctx, cl, vn) + if err != nil { + return nil, fmt.Errorf("failed to retrieve label selectors: %w", err) + } + return &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: vn.Name, + Labels: l, + }, + }, nil +} + +// GetLabelSelectors returns the labels that can be used to target a remote cluster. +// If virtualnode spec.CreateNode is true, the labels are taken from the created node. +// If virtualnode spec.CreateNode is false, the labels are taken from the virtual node spec.Labels and spec.Template . +func GetLabelSelectors(ctx context.Context, cl client.Client, vn *virtualkubeletv1alpha1.VirtualNode) (labels.Set, error) { + n, err := getters.GetNodeFromVirtualNode(ctx, cl, vn) + switch { + case errors.IsNotFound(err): + return labels.Merge(vn.Spec.Labels, labels.Set{ + liqoconsts.RemoteClusterID: vn.Spec.ClusterIdentity.ClusterID, + liqoconsts.StorageAvailableLabel: strconv.FormatBool(len(vn.Spec.StorageClasses) == 0), + }), nil + case err != nil: + return nil, fmt.Errorf("failed to retrieve node %s from VirtualNode %s: %w", n.Name, vn.Name, err) + default: + return n.Labels, nil + } +}