diff --git a/cmd/liqoctl/cmd/root.go b/cmd/liqoctl/cmd/root.go index 660425fe02..92ce5c45ee 100644 --- a/cmd/liqoctl/cmd/root.go +++ b/cmd/liqoctl/cmd/root.go @@ -39,6 +39,7 @@ import ( "github.com/liqotech/liqo/pkg/liqoctl/rest/identity" "github.com/liqotech/liqo/pkg/liqoctl/rest/kubeconfig" "github.com/liqotech/liqo/pkg/liqoctl/rest/nonce" + peeringuser "github.com/liqotech/liqo/pkg/liqoctl/rest/peering-user" "github.com/liqotech/liqo/pkg/liqoctl/rest/publickey" "github.com/liqotech/liqo/pkg/liqoctl/rest/resourceslice" "github.com/liqotech/liqo/pkg/liqoctl/rest/tenant" @@ -56,6 +57,7 @@ var liqoResources = []rest.APIProvider{ publickey.PublicKey, tenant.Tenant, nonce.Nonce, + peeringuser.PeeringUser, identity.Identity, resourceslice.ResourceSlice, kubeconfig.Kubeconfig, diff --git a/cmd/liqoctl/cmd/unpeer.go b/cmd/liqoctl/cmd/unpeer.go index 7f2b366097..7e01afa8f3 100644 --- a/cmd/liqoctl/cmd/unpeer.go +++ b/cmd/liqoctl/cmd/unpeer.go @@ -38,7 +38,7 @@ offloaded workloads to be rescheduled. The Identity and Tenant are respectively removed from the consumer and provider clusters, and the networking between the two clusters is destroyed. -The reverse peering, if any, is preserved, and the remote cluster can continue +The reverse peering, if any, is preserved, and the remote cluster can continue offloading workloads to its virtual node representing the local cluster. Examples: @@ -66,7 +66,7 @@ func newUnpeerCommand(ctx context.Context, f *factory.Factory) *cobra.Command { cmd.PersistentFlags().DurationVar(&options.Timeout, "timeout", 120*time.Second, "Timeout for unpeering completion") cmd.PersistentFlags().BoolVar(&options.Wait, "wait", true, "Wait for resource to be deleted before returning") - cmd.PersistentFlags().BoolVar(&options.KeepNamespaces, "keep-namespaces", false, "Keep tenant namespaces after unpeering") + cmd.PersistentFlags().BoolVar(&options.DeleteNamespace, "delete-namespaces", false, "Delete the tenant namespace after unpeering") options.LocalFactory.AddFlags(cmd.PersistentFlags(), cmd.RegisterFlagCompletionFunc) options.RemoteFactory.AddFlags(cmd.PersistentFlags(), cmd.RegisterFlagCompletionFunc) diff --git a/deployments/liqo/files/liqo-peering-user-ClusterRole.yaml b/deployments/liqo/files/liqo-peering-user-ClusterRole.yaml new file mode 100644 index 0000000000..4524e7f00d --- /dev/null +++ b/deployments/liqo/files/liqo-peering-user-ClusterRole.yaml @@ -0,0 +1,65 @@ +rules: +- apiGroups: + - "networking.liqo.io" + resources: + - "configurations" + - "gatewayclients" + - "gatewayservers" + - "publickeies" + verbs: + - "create" + - "update" + - "get" + - "list" + - "delete" +- apiGroups: + - "networking.liqo.io" + resources: + - "connections" + verbs: + - "get" + - "list" +- apiGroups: + - "networking.liqo.io" + resources: + - "gatewayclients/status" + - "gatewayservers/status" + verbs: + - "get" +- apiGroups: + - "" + resources: + - "configmaps" + - "secrets" + verbs: + - "create" + - "get" + - "list" + - "delete" +- apiGroups: + - "" + resources: + - "services" + verbs: + - "get" +- apiGroups: + - "apps" + resources: + - "deployments" + verbs: + - "get" +- apiGroups: + - "ipam.liqo.io" + resources: + - "ips" + verbs: + - "create" + - "update" + - "get" + - "delete" +- apiGroups: + - "authentication.liqo.io" + resources: + - "tenants/status" + verbs: + - "get" diff --git a/deployments/liqo/files/liqo-peering-user-Role.yaml b/deployments/liqo/files/liqo-peering-user-Role.yaml new file mode 100644 index 0000000000..19d372cec3 --- /dev/null +++ b/deployments/liqo/files/liqo-peering-user-Role.yaml @@ -0,0 +1,17 @@ +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch +- apiGroups: + - "networking.liqo.io" + resources: + - "wggatewayservertemplates" + - "wggatewayclienttemplates" + verbs: + - "get" + - "list" diff --git a/deployments/liqo/templates/liqo-peer-rbac.yaml b/deployments/liqo/templates/liqo-peer-rbac.yaml new file mode 100644 index 0000000000..807dad2f17 --- /dev/null +++ b/deployments/liqo/templates/liqo-peer-rbac.yaml @@ -0,0 +1,17 @@ +{{- $peeringroles := (merge (dict "name" "peering-user" "module" "peering-user") .) -}} + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "liqo.prefixedName" $peeringroles}} + labels: + {{- include "liqo.labels" $peeringroles| nindent 4 }} +{{ .Files.Get (include "liqo.cluster-role-filename" (dict "prefix" ( include "liqo.prefixedName" $peeringroles))) }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "liqo.prefixedName" $peeringroles}} + labels: + {{- include "liqo.labels" $peeringroles| nindent 4 }} +{{ .Files.Get (include "liqo.role-filename" (dict "prefix" ( include "liqo.prefixedName" $peeringroles))) }} \ No newline at end of file diff --git a/docs/usage/peer.md b/docs/usage/peer.md index acc42c63fd..fddf19d450 100644 --- a/docs/usage/peer.md +++ b/docs/usage/peer.md @@ -56,12 +56,48 @@ You can configure and fine-tune each module separately using the individual comm For the majority and the cases the `liqoctl peer` is enough. However, **to know the best strategy for each case and the requirements of each approach, check the [peering strategies guide](/advanced/peering-strategies.md)**. -### Peering establishment +### Getting the required permissions to establish a peering -To proceed, ensure that you are operating in the *consumer* cluster, and then issue the *liqoctl peer* command: +To create a peering with a *provider* cluster, you will require a kubeconfig with a set of permissions to establish a connection with it. + +The [liqoctl](../installation/liqoctl.md) CLI tool provides utility functions to manage the permissions of users able to create a peering connection with the current cluster. + +**From the *provider* cluster**, you can run the following command to generate a *kubeconfig*: + +```bash +liqoctl generate peering-user \ + --kubeconfig $PROVIDER_KUBECONFIG_PATH \ + --consumer-cluster-id $CONSUMER_CLUSTER_ID > $CONSUMER_KUBECONFIG_PATH +``` + +```{warning} +Once you generate the *kubeconfig*, take note of it as it will not be stored by Liqo. +If you lose it, you will need to delete and recreate it. +``` + +This command will create a *kubeconfig* with **the minimum permissions to create and destroy a peering with the current cluster** from a cluster with ID `$CONSUMER_CLUSTER_ID`. + +You are allowed to have a single peering user for each consumer cluster, so you will not be able to create a new kubeconfig for the same consumer cluster unless you delete the previous one. + +````{admonition} Note +To delete a peering user for the consumer cluster with ID `$CONSUMER_CLUSTER_ID`, run: + +```bash +liqoctl delete peering-user \ + --consumer-cluster-id $CONSUMER_CLUSTER_ID +``` + +**Once you delete a peering user, its kubeconfig will not be valid anymore, even though a new peering user for the same cluster is created.** +```` + +### Establish a peering connection + +To establish a peering connection between two clusters, ensure that you are operating in the *consumer* cluster, then issue the *liqoctl peer* command: ```bash -liqoctl --kubeconfig=$CONSUMER_KUBECONFIG_PATH peer --remote-kubeconfig $PROVIDER_KUBECONFIG_PATH +liqoctl peer \ + --kubeconfig=$CONSUMER_KUBECONFIG_PATH \ + --remote-kubeconfig $PROVIDER_KUBECONFIG_PATH ``` ```{warning} diff --git a/pkg/consts/authentication.go b/pkg/consts/authentication.go index cb7deda195..4fa9ef1d5e 100644 --- a/pkg/consts/authentication.go +++ b/pkg/consts/authentication.go @@ -54,4 +54,7 @@ const ( // RenewAnnotation is the value of the annotation that enables the renewal of a resource. RenewAnnotation = "liqo.io/renew" + + // PeeringUserNameLabelKey labels all the resources created to grant peering permissions to the user doing a pering toward this cluster. + PeeringUserNameLabelKey = "liqo.io/peering-user-name" ) diff --git a/pkg/liqo-controller-manager/authentication/csr.go b/pkg/liqo-controller-manager/authentication/csr.go index dc3d26521d..c6efed8214 100644 --- a/pkg/liqo-controller-manager/authentication/csr.go +++ b/pkg/liqo-controller-manager/authentication/csr.go @@ -68,6 +68,26 @@ func GenerateCSRForControlPlane(key ed25519.PrivateKey, clusterID liqov1beta1.Cl return generateCSR(key, CommonNameControlPlaneCSR(clusterID), OrganizationControlPlaneCSR()) } +// GenerateCSRForPeerUser generates a new CSR given a private key and the clusterID from which the peering will start. +func GenerateCSRForPeerUser(key ed25519.PrivateKey, clusterID liqov1beta1.ClusterID) (csrBytes []byte, userCN string, err error) { + userCN, err = commonNamePeerUser(clusterID) + if err != nil { + return nil, "", fmt.Errorf("unable to generate user CN: %w", err) + } + + csrBytes, err = generateCSR(key, userCN, OrganizationControlPlaneCSR()) + return +} + +// commonNamePeerUser returns the common name for the user creating the peering. To avoid reuses of the same name, a suffix is added. +func commonNamePeerUser(clusterID liqov1beta1.ClusterID) (string, error) { + randSuffix := make([]byte, 16) + if _, err := rand.Read(randSuffix); err != nil { + return "", err + } + return fmt.Sprintf("liqo-peer-user-%s-%x", clusterID, randSuffix), nil +} + // CommonNameControlPlaneCSR returns the common name for a control plane CSR. func CommonNameControlPlaneCSR(clusterID liqov1beta1.ClusterID) string { return string(clusterID) diff --git a/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go b/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go index a409ccd691..6ec96a528b 100644 --- a/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go +++ b/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go @@ -138,7 +138,7 @@ func (r *TenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res if authv1beta1.GetAuthzPolicyValue(tenant.Spec.AuthzPolicy) != authv1beta1.TolerateNoHandshake { // get the nonce for the tenant - nonceSecret, err := getters.GetNonceSecretByClusterID(ctx, r.Client, clusterID) + nonceSecret, err := getters.GetNonceSecretByClusterID(ctx, r.Client, clusterID, corev1.NamespaceAll) if err != nil { klog.Errorf("Unable to get the nonce for the Tenant %q: %s", req.Name, err) r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "NonceNotFound", err.Error()) diff --git a/pkg/liqo-controller-manager/authentication/utils/nonce.go b/pkg/liqo-controller-manager/authentication/utils/nonce.go index a0f7cc084f..7e62b80e9e 100644 --- a/pkg/liqo-controller-manager/authentication/utils/nonce.go +++ b/pkg/liqo-controller-manager/authentication/utils/nonce.go @@ -47,7 +47,7 @@ func EnsureNonceSecret(ctx context.Context, cl client.Client, // already a nonce secret in the tenant namespace. func EnsureSignedNonceSecret(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID, tenantNamespace string, nonce *string) error { - nonceSecret, err := getters.GetSignedNonceSecretByClusterID(ctx, cl, remoteClusterID) + nonceSecret, err := getters.GetSignedNonceSecretByClusterID(ctx, cl, remoteClusterID, tenantNamespace) switch { case errors.IsNotFound(err): // Secret not found. Create it given the provided nonce. @@ -80,8 +80,8 @@ func EnsureSignedNonceSecret(ctx context.Context, cl client.Client, } // RetrieveNonce retrieves the nonce from the secret in the tenant namespace. -func RetrieveNonce(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID) ([]byte, error) { - nonce, err := getters.GetNonceSecretByClusterID(ctx, cl, remoteClusterID) +func RetrieveNonce(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID, tenantNs string) ([]byte, error) { + nonce, err := getters.GetNonceSecretByClusterID(ctx, cl, remoteClusterID, tenantNs) if err != nil { return nil, fmt.Errorf("unable to get nonce secret: %w", err) } @@ -90,8 +90,8 @@ func RetrieveNonce(ctx context.Context, cl client.Client, remoteClusterID liqov1 } // RetrieveSignedNonce retrieves the signed nonce from the secret in the tenant namespace. -func RetrieveSignedNonce(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID) ([]byte, error) { - secret, err := getters.GetSignedNonceSecretByClusterID(ctx, cl, remoteClusterID) +func RetrieveSignedNonce(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID, tenantNs string) ([]byte, error) { + secret, err := getters.GetSignedNonceSecretByClusterID(ctx, cl, remoteClusterID, tenantNs) if err != nil { return nil, fmt.Errorf("unable to get signed nonce secret: %w", err) } diff --git a/pkg/liqo-controller-manager/offloading/virtualnode-controller/virtualkubelet.go b/pkg/liqo-controller-manager/offloading/virtualnode-controller/virtualkubelet.go index 30cbda7e88..6116bf339b 100644 --- a/pkg/liqo-controller-manager/offloading/virtualnode-controller/virtualkubelet.go +++ b/pkg/liqo-controller-manager/offloading/virtualnode-controller/virtualkubelet.go @@ -171,12 +171,14 @@ func (r *VirtualNodeReconciler) ensureVirtualKubeletDeploymentAbsence( return err } + crbName := k8strings.ShortenString(fmt.Sprintf("%s%s", vkMachinery.CRBPrefix, virtualNode.Name), 253) err = r.Client.Delete(ctx, &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{ - Name: k8strings.ShortenString(fmt.Sprintf("%s%s", vkMachinery.CRBPrefix, virtualNode.Name), 253), + Name: crbName, }}) if client.IgnoreNotFound(err) != nil { return err } + klog.Info(fmt.Sprintf("[%v] Deleted virtual-kubelet CRB %s", virtualNode.Spec.ClusterID, crbName)) err = r.Client.Delete(ctx, &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ Name: virtualNode.Name, Namespace: virtualNode.Namespace, diff --git a/pkg/liqoctl/authenticate/cluster.go b/pkg/liqoctl/authenticate/cluster.go index c2e1086142..b61acfc3b5 100644 --- a/pkg/liqoctl/authenticate/cluster.go +++ b/pkg/liqoctl/authenticate/cluster.go @@ -104,13 +104,13 @@ func (c *Cluster) EnsureNonce(ctx context.Context) ([]byte, error) { s.Success("Nonce secret ensured") // Wait for secret to be filled with the nonce. - if err := c.waiter.ForNonce(ctx, c.RemoteClusterID, false); err != nil { + if err := c.waiter.ForNonce(ctx, c.RemoteClusterID, c.TenantNamespace, false); err != nil { return nil, err } // Retrieve nonce from secret. s = c.local.Printer.StartSpinner("Retrieving nonce") - nonceValue, err := authutils.RetrieveNonce(ctx, c.local.CRClient, c.RemoteClusterID) + nonceValue, err := authutils.RetrieveNonce(ctx, c.local.CRClient, c.RemoteClusterID, c.TenantNamespace) if err != nil { s.Fail(fmt.Sprintf("Unable to retrieve nonce: %v", output.PrettyErr(err))) return nil, err @@ -135,13 +135,13 @@ func (c *Cluster) EnsureSignedNonce(ctx context.Context, nonce []byte) ([]byte, s.Success("Signed nonce secret ensured") // Wait for secret to be filled with the signed nonce. - if err := c.waiter.ForSignedNonce(ctx, c.RemoteClusterID, false); err != nil { + if err := c.waiter.ForSignedNonce(ctx, c.RemoteClusterID, false, c.TenantNamespace); err != nil { return nil, err } // Retrieve signed nonce from secret. s = c.local.Printer.StartSpinner("Retrieving signed nonce") - signedNonceValue, err := authutils.RetrieveSignedNonce(ctx, c.local.CRClient, c.RemoteClusterID) + signedNonceValue, err := authutils.RetrieveSignedNonce(ctx, c.local.CRClient, c.RemoteClusterID, c.TenantNamespace) if err != nil { s.Fail(fmt.Sprintf("Unable to retrieve signed nonce: %v", output.PrettyErr(err))) return nil, err diff --git a/pkg/liqoctl/info/localstatus/local_info.go b/pkg/liqoctl/info/localstatus/local_info.go index 721094f726..94411d7f66 100644 --- a/pkg/liqoctl/info/localstatus/local_info.go +++ b/pkg/liqoctl/info/localstatus/local_info.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" + "github.com/liqotech/liqo/pkg/consts" "github.com/liqotech/liqo/pkg/liqoctl/info" "github.com/liqotech/liqo/pkg/liqoctl/output" liqoctlutils "github.com/liqotech/liqo/pkg/liqoctl/utils" @@ -46,10 +47,6 @@ type InstallationChecker struct { data Installation } -const ( - ctrlManagerContainerName = "controller-manager" -) - // Collect data about the local installation of Liqo. func (l *InstallationChecker) Collect(ctx context.Context, options info.Options) { // Get the cluster ID of the local cluster @@ -64,7 +61,7 @@ func (l *InstallationChecker) Collect(ctx context.Context, options info.Options) if err != nil { l.AddCollectionError(fmt.Errorf("unable to get Liqo version and cluster labels: %w", err)) } else { - ctrlContainer, err := l.getCtrlManagerContainer(ctrlDeployment) + ctrlContainer, err := liqoctlutils.GetCtrlManagerContainer(ctrlDeployment) if err != nil { l.AddCollectionError(fmt.Errorf("unable to get Liqo instance info: %w", err)) } else { @@ -143,22 +140,10 @@ func (l *InstallationChecker) collectClusterLabels(ctrlContainer *corev1.Contain } func (l *InstallationChecker) collectLiqoVersion(ctrlDeployment *appsv1.Deployment) error { - version, err := getters.GetContainerImageVersion(ctrlDeployment.Spec.Template.Spec.Containers, ctrlManagerContainerName) + version, err := getters.GetContainerImageVersion(ctrlDeployment.Spec.Template.Spec.Containers, consts.ControllerManagerAppName) if err != nil { return err } l.data.Version = version return nil } - -func (l *InstallationChecker) getCtrlManagerContainer(ctrlDeployment *appsv1.Deployment) (*corev1.Container, error) { - // Get the container of the controller manager - containers := ctrlDeployment.Spec.Template.Spec.Containers - for i := range containers { - if containers[i].Name == ctrlManagerContainerName { - return &containers[i], nil - } - } - - return nil, fmt.Errorf("invalid controller manager deployment: no container with name %q found", ctrlManagerContainerName) -} diff --git a/pkg/liqoctl/output/output.go b/pkg/liqoctl/output/output.go index d0321c6fe5..bb6a7fe6c5 100644 --- a/pkg/liqoctl/output/output.go +++ b/pkg/liqoctl/output/output.go @@ -270,7 +270,10 @@ func NewGlobalPrinter(scoped, verbose bool) *Printer { } func newPrinter(scope string, color pterm.Color, scoped, verbose bool) *Printer { - generic := &pterm.PrefixPrinter{MessageStyle: pterm.NewStyle(pterm.FgDefault)} + generic := &pterm.PrefixPrinter{ + MessageStyle: pterm.NewStyle(pterm.FgDefault), + Writer: os.Stderr, + } if scoped { generic = generic.WithScope(pterm.Scope{Text: scope, Style: pterm.NewStyle(pterm.FgGray)}) @@ -311,6 +314,7 @@ func newPrinter(scope string, color pterm.Color, scoped, verbose bool) *Printer ShowTimer: true, TimerRoundingFactor: time.Second, TimerStyle: &pterm.ThemeDefault.TimerStyle, + Writer: os.Stderr, } printer.BulletList = &pterm.BulletListPrinter{} diff --git a/pkg/liqoctl/peer/handler.go b/pkg/liqoctl/peer/handler.go index 00a13afc36..4be9a5d2ac 100644 --- a/pkg/liqoctl/peer/handler.go +++ b/pkg/liqoctl/peer/handler.go @@ -86,7 +86,7 @@ func (o *Options) RunPeer(ctx context.Context) error { // Ensure networking if !o.NetworkingDisabled { if err := ensureNetworking(ctx, o); err != nil { - o.LocalFactory.PrinterGlobal.Error.Println("unable to ensure networking") + o.LocalFactory.PrinterGlobal.Error.Printfln("Unable to ensure networking: %v", err) return err } } diff --git a/pkg/liqoctl/rest/nonce/create.go b/pkg/liqoctl/rest/nonce/create.go index 53ed61d386..7cc4c513b8 100644 --- a/pkg/liqoctl/rest/nonce/create.go +++ b/pkg/liqoctl/rest/nonce/create.go @@ -105,13 +105,13 @@ func (o *Options) handleCreate(ctx context.Context) error { s.Success("Nonce created") // Wait for secret to be filled with the nonce. - if err := waiter.ForNonce(ctx, o.clusterID.GetClusterID(), false); err != nil { + if err := waiter.ForNonce(ctx, o.clusterID.GetClusterID(), tenantNs.GetName(), false); err != nil { return err } // Retrieve nonce from secret. s = opts.Printer.StartSpinner("Retrieving nonce") - nonceValue, err := authutils.RetrieveNonce(ctx, opts.CRClient, o.clusterID.GetClusterID()) + nonceValue, err := authutils.RetrieveNonce(ctx, opts.CRClient, o.clusterID.GetClusterID(), tenantNs.GetName()) if err != nil { s.Fail(fmt.Sprintf("Unable to retrieve nonce: %v", output.PrettyErr(err))) return err diff --git a/pkg/liqoctl/rest/nonce/get.go b/pkg/liqoctl/rest/nonce/get.go index 6404df4c31..b68dd76aae 100644 --- a/pkg/liqoctl/rest/nonce/get.go +++ b/pkg/liqoctl/rest/nonce/get.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/runtime" authutils "github.com/liqotech/liqo/pkg/liqo-controller-manager/authentication/utils" @@ -67,7 +68,7 @@ func (o *Options) Get(ctx context.Context, options *rest.GetOptions) *cobra.Comm func (o *Options) handleGet(ctx context.Context) error { opts := o.getOptions - nonceValue, err := authutils.RetrieveNonce(ctx, opts.CRClient, o.clusterID.GetClusterID()) + nonceValue, err := authutils.RetrieveNonce(ctx, opts.CRClient, o.clusterID.GetClusterID(), corev1.NamespaceAll) if err != nil { opts.Printer.CheckErr(fmt.Errorf("unable to retrieve nonce: %v", output.PrettyErr(err))) return err diff --git a/pkg/liqoctl/rest/peering-user/create.go b/pkg/liqoctl/rest/peering-user/create.go new file mode 100644 index 0000000000..39ddf67f89 --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/create.go @@ -0,0 +1,28 @@ +// Copyright 2019-2025 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 peeringuser + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// Create implements the create command. +func (o *Options) Create(_ context.Context, _ *rest.CreateOptions) *cobra.Command { + panic("not implemented") +} diff --git a/pkg/liqoctl/rest/peering-user/delete.go b/pkg/liqoctl/rest/peering-user/delete.go new file mode 100644 index 0000000000..4db889da40 --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/delete.go @@ -0,0 +1,74 @@ +// Copyright 2019-2025 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 peeringuser + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/runtime" + + liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" + "github.com/liqotech/liqo/pkg/liqoctl/output" + "github.com/liqotech/liqo/pkg/liqoctl/rest" + "github.com/liqotech/liqo/pkg/liqoctl/rest/peering-user/userfactory" +) + +const liqoctlDeletePeeringUserHelp = `elete an existing user with the permissions to peer with this cluster. + +Delete a peering user, so that it will no longer be able to peer with this cluster from the cluster with the given Cluster ID. +The previous credentials will be invalidated, and cannot be used anymore, even if the user is recreated. + +Examples: + $ {{ .Executable }} delete peering-user --consumer-cluster-id=` + +// Delete deletes a user. +func (o *Options) Delete(ctx context.Context, options *rest.DeleteOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "peering-user", + Short: "Delete an existing user with the permissions to peer with this cluster", + Long: liqoctlDeletePeeringUserHelp, + Args: cobra.NoArgs, + + PreRun: func(_ *cobra.Command, _ []string) { + o.deleteOptions = options + }, + + Run: func(_ *cobra.Command, _ []string) { + output.ExitOnErr(o.handleDelete(ctx)) + }, + } + + cmd.Flags().Var(&o.clusterID, "consumer-cluster-id", "The cluster ID of the cluster from which peering has been performed") + + runtime.Must(cmd.MarkFlagRequired("consumer-cluster-id")) + + return cmd +} + +func (o *Options) handleDelete(ctx context.Context) error { + opts := o.deleteOptions + clusterID := liqov1beta1.ClusterID(*o.clusterID.ClusterID) + + if err := userfactory.RemovePermissions(ctx, opts.CRClient, clusterID); err != nil { + wErr := fmt.Errorf("unable to delete peering user: %w", err) + opts.Printer.Error.Println(wErr) + return wErr + } + + opts.Printer.Success.Printfln("Peering user for cluster with ID %q deleted successfully", clusterID) + return nil +} diff --git a/pkg/liqoctl/rest/peering-user/doc.go b/pkg/liqoctl/rest/peering-user/doc.go new file mode 100644 index 0000000000..e7da1d5976 --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019-2025 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 peeringuser contains the rest API commands to allow liqoctl to create an authentication token to authenticate to this cluster. +package peeringuser diff --git a/pkg/liqoctl/rest/peering-user/generate.go b/pkg/liqoctl/rest/peering-user/generate.go new file mode 100644 index 0000000000..c6d58c896b --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/generate.go @@ -0,0 +1,90 @@ +// Copyright 2019-2025 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 peeringuser + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/runtime" + + liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" + "github.com/liqotech/liqo/pkg/liqoctl/output" + "github.com/liqotech/liqo/pkg/liqoctl/rest" + "github.com/liqotech/liqo/pkg/liqoctl/rest/peering-user/userfactory" + tenantnamespace "github.com/liqotech/liqo/pkg/tenantNamespace" +) + +const liqoctlGeneratePeeringUserHelp = `Generate a new user with the permissions to peer with this cluster. + +This command generates a user with the minimum permissions to peer with this cluster, from the cluster with +the given cluster ID, and returns a kubeconfig to be used to create or destroy the peering. + +Examples: + $ {{ .Executable }} generate peering-user --consumer-cluster-id=` + +// Generate generates a Nonce. +func (o *Options) Generate(ctx context.Context, options *rest.GenerateOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "peering-user", + Short: "Generate a new user with the permissions to peer with this cluster", + Long: liqoctlGeneratePeeringUserHelp, + Args: cobra.NoArgs, + + PreRun: func(_ *cobra.Command, _ []string) { + o.generateOptions = options + + o.namespaceManager = tenantnamespace.NewManager(options.KubeClient, options.CRClient.Scheme()) + }, + + Run: func(_ *cobra.Command, _ []string) { + output.ExitOnErr(o.handleGenerate(ctx)) + }, + } + + cmd.Flags().Var(&o.clusterID, "consumer-cluster-id", "The cluster ID of the cluster from which peering will be performed") + + runtime.Must(cmd.MarkFlagRequired("consumer-cluster-id")) + + return cmd +} + +func (o *Options) handleGenerate(ctx context.Context) error { + opts := o.generateOptions + + clusterID := liqov1beta1.ClusterID(*o.clusterID.ClusterID) + opts.Printer.Warning.Println("Note that this functionality is currently not supported in EKS clusters") + opts.Printer.Warning.Println("Please take note of this kubeconfig as it is not stored.") + opts.Printer.Warning.Printfln("Note that it can only be used to peer with this cluster from a cluster with ID %s", clusterID) + + tenantNs, err := o.namespaceManager.CreateNamespace(ctx, clusterID) + if err != nil { + wErr := fmt.Errorf("unable to create the tenant namespace: %w", err) + opts.Printer.Error.Println(wErr) + return wErr + } + + spinner := opts.Printer.StartSpinner("Generating a user for peering with this cluster") + kubeconfig, err := userfactory.GeneratePeerUser(ctx, clusterID, tenantNs.Name, opts.Factory) + if err != nil { + spinner.Fail(err) + return err + } + spinner.Success("User generated successfully") + + fmt.Println(kubeconfig) + return nil +} diff --git a/pkg/liqoctl/rest/peering-user/get.go b/pkg/liqoctl/rest/peering-user/get.go new file mode 100644 index 0000000000..327166cd7e --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/get.go @@ -0,0 +1,28 @@ +// Copyright 2019-2025 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 peeringuser + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// Get implements the get command. +func (o *Options) Get(_ context.Context, _ *rest.GetOptions) *cobra.Command { + panic("not implemented") +} diff --git a/pkg/liqoctl/rest/peering-user/types.go b/pkg/liqoctl/rest/peering-user/types.go new file mode 100644 index 0000000000..d42a5f696b --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/types.go @@ -0,0 +1,45 @@ +// Copyright 2019-2025 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 peeringuser + +import ( + "github.com/liqotech/liqo/pkg/liqoctl/rest" + tenantnamespace "github.com/liqotech/liqo/pkg/tenantNamespace" + "github.com/liqotech/liqo/pkg/utils/args" +) + +// Options encapsulates the arguments of the token command. +type Options struct { + generateOptions *rest.GenerateOptions + deleteOptions *rest.DeleteOptions + namespaceManager tenantnamespace.Manager + + clusterID args.ClusterIDFlags +} + +var _ rest.API = &Options{} + +// PeeringUser returns the rest API for the token command. +func PeeringUser() rest.API { + return &Options{} +} + +// APIOptions returns the APIOptions for the nonce API. +func (o *Options) APIOptions() *rest.APIOptions { + return &rest.APIOptions{ + EnableGenerate: true, + EnableDelete: true, + } +} diff --git a/pkg/liqoctl/rest/peering-user/update.go b/pkg/liqoctl/rest/peering-user/update.go new file mode 100644 index 0000000000..f973f9ab35 --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/update.go @@ -0,0 +1,28 @@ +// Copyright 2019-2025 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 peeringuser + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// Update implements the update command. +func (o *Options) Update(_ context.Context, _ *rest.UpdateOptions) *cobra.Command { + panic("not implemented") +} diff --git a/pkg/liqoctl/rest/peering-user/userfactory/cert.go b/pkg/liqoctl/rest/peering-user/userfactory/cert.go new file mode 100644 index 0000000000..09fed6aa35 --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/userfactory/cert.go @@ -0,0 +1,232 @@ +// Copyright 2019-2025 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 userfactory + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + certv1 "k8s.io/api/certificates/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" + + liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" + "github.com/liqotech/liqo/pkg/consts" + "github.com/liqotech/liqo/pkg/liqo-controller-manager/authentication" + "github.com/liqotech/liqo/pkg/liqoctl/factory" + liqoctlutils "github.com/liqotech/liqo/pkg/liqoctl/utils" + "github.com/liqotech/liqo/pkg/utils/apiserver" + certificateSigningRequest "github.com/liqotech/liqo/pkg/utils/csr" + "github.com/liqotech/liqo/pkg/utils/getters" +) + +// GeneratePeerUser generates a new user to peer with the local cluster and returns its kubeconfig. +func GeneratePeerUser(ctx context.Context, clusterID liqov1beta1.ClusterID, tenantNsName string, opts *factory.Factory) (string, error) { + if exists, err := IsExistingPeerUser(ctx, opts.CRClient, clusterID); err != nil { + return "", fmt.Errorf("unable to check if the user already exists: %w", err) + } else if exists { + return "", fmt.Errorf("a user to peer from cluster with ID %q already exists. Please delete if first before creting a new one."+ + "You can delete the previous secret via 'liqoctl delete peering-user --consumer-cluster-id %s'", clusterID, clusterID) + } + + // Get the certification authority + ca, err := apiserver.RetrieveAPIServerCA(opts.RESTConfig, nil, false) + if err != nil { + return "", fmt.Errorf("unable to get the API server CA: %w", err) + } + + // Forge a new pair of keys. + _, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", fmt.Errorf("error while generating token credentials: %w", err) + } + + // Generate a CSR with the newly created keys. + csr, userCN, err := authentication.GenerateCSRForPeerUser(private, clusterID) + if err != nil { + return "", fmt.Errorf("error while generating the csr for the token credentials: %w", err) + } + + // Sign the csr to generate the certificate + cert, err := generateSignedCert(ctx, opts.CRClient, opts.KubeClient, csr, clusterID) + if err != nil { + return "", fmt.Errorf("unable to generate certificate for the user: %w", err) + } + + if err := EnsureRoles(ctx, opts.CRClient, clusterID, userCN, tenantNsName); err != nil { + return "", fmt.Errorf("unable to ensure roles: %w", err) + } + + // Convert the private key in PEM format + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(private) + if err != nil { + return "", fmt.Errorf("unable to parse private key: %w", err) + } + privatePEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}) + + // Get K8S API Server address + apiAddr, err := getAPIServerAddress(ctx, opts.CRClient, opts.LiqoNamespace) + if err != nil { + return "", fmt.Errorf("unable to get the API Server addr: %w", err) + } + + kubeconfig, err := generateKubeconfig(string(clusterID), ca, cert, privatePEM, apiAddr) + if err != nil { + return "", fmt.Errorf("unable to generate kubeconfig: %w", err) + } + + return kubeconfig, nil +} + +// GetUserNameFromClusterID returns the username of the peering user for the given clusterID. +func GetUserNameFromClusterID(clusterID liqov1beta1.ClusterID) string { + return fmt.Sprintf("liqo-peer-user-%s", clusterID) +} + +func generateKubeconfig(clusterID string, ca, cert, private []byte, apiAddr string) (string, error) { + kubeconfig := api.NewConfig() + + userName := fmt.Sprintf("%s-user", clusterID) + // Set up the cluster + cluster := api.NewCluster() + cluster.Server = apiAddr + cluster.CertificateAuthorityData = ca + kubeconfig.Clusters[clusterID] = cluster + + // Set up the user + user := api.NewAuthInfo() + user.ClientCertificateData = cert + user.ClientKeyData = private + kubeconfig.AuthInfos[userName] = user + + // The up the clusterContext + clusterContext := api.NewContext() + clusterContext.Cluster = clusterID + clusterContext.AuthInfo = userName + kubeconfig.Contexts[clusterID] = clusterContext + kubeconfig.CurrentContext = clusterID + + // Convert the kubeconfig to YAML + kubeconfigBytes, err := clientcmd.Write(*kubeconfig) + if err != nil { + return "", fmt.Errorf("failed to build kubeconfig: %w", err) + } + + return string(kubeconfigBytes), nil +} + +func getAPIServerAddress(ctx context.Context, c client.Client, liqoNamespaceName string) (string, error) { + // Get the controller manager deployment + ctrlDeployment, err := getters.GetControllerManagerDeployment(ctx, c, liqoNamespaceName) + if err != nil { + return "", err + } + + // Get the controller manager container + ctrlContainer, err := liqoctlutils.GetCtrlManagerContainer(ctrlDeployment) + if err != nil { + return "", err + } + + // Get the URL of the K8s API + apiServerAddressOverride, _ := liqoctlutils.ExtractValuesFromArgumentList("--api-server-address-override", ctrlContainer.Args) + apiAddr, err := apiserver.GetURL(ctx, c, apiServerAddressOverride) + if err != nil { + return "", err + } + + return apiAddr, nil +} + +// generateSignedCert generates a new signed certificate to create a peering with the local cluster. +func generateSignedCert( + ctx context.Context, + c client.Client, + clientset kubernetes.Interface, + csr []byte, + clusterID liqov1beta1.ClusterID, +) ([]byte, error) { + userName := GetUserNameFromClusterID(clusterID) + cert := &certv1.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: userName, + Labels: map[string]string{ + consts.PeeringUserNameLabelKey: userName, + }, + }, + Spec: certv1.CertificateSigningRequestSpec{ + Groups: []string{ + "system:authenticated", + }, + SignerName: certv1.KubeAPIServerClientSignerName, + Request: csr, + Usages: []certv1.KeyUsage{ + certv1.UsageDigitalSignature, + certv1.UsageKeyEncipherment, + certv1.UsageClientAuth, + }, + }, + } + + cert, err := clientset.CertificatesV1().CertificateSigningRequests().Create(ctx, cert, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + + // approve the CertificateSigningRequest + if err = certificateSigningRequest.Approve(clientset, cert, "IdentityManagerApproval", + "This CSR was approved by liqoctl generate token"); err != nil { + return nil, err + } + + // retrieve the certificate issued by the Kubernetes issuer in the CSR (with a 30 seconds timeout) + ctxC, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + return getSignedCSRBlocker(ctxC, c, cert.Name) +} + +// getSignedCSRBlocker waits until the csr with the given name has been signed and it returned the certificate. +func getSignedCSRBlocker(ctx context.Context, c client.Client, csrName string) ([]byte, error) { + var certificate []byte + err := wait.PollUntilContextCancel(ctx, 1*time.Second, true, func(ctx context.Context) (done bool, err error) { + var csr certv1.CertificateSigningRequest + if err := c.Get(ctx, client.ObjectKey{Name: csrName}, &csr); err != nil { + return false, err + } + if len(csr.Status.Certificate) > 0 { + certificate = csr.Status.Certificate + return true, nil + } + + return false, nil + }) + + if err != nil { + return nil, fmt.Errorf("failed waiting for CSR to be signed: %w", err) + } + + return certificate, nil +} diff --git a/pkg/liqoctl/rest/peering-user/userfactory/doc.go b/pkg/liqoctl/rest/peering-user/userfactory/doc.go new file mode 100644 index 0000000000..240c206c67 --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/userfactory/doc.go @@ -0,0 +1,17 @@ +// Copyright 2019-2025 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 userfactory contains the logic to create a new user for the peering. +package userfactory diff --git a/pkg/liqoctl/rest/peering-user/userfactory/roles.go b/pkg/liqoctl/rest/peering-user/userfactory/roles.go new file mode 100644 index 0000000000..87943cc30b --- /dev/null +++ b/pkg/liqoctl/rest/peering-user/userfactory/roles.go @@ -0,0 +1,292 @@ +// Copyright 2019-2025 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 userfactory + +import ( + "context" + "fmt" + + certv1 "k8s.io/api/certificates/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + + liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" + "github.com/liqotech/liqo/pkg/consts" +) + +var peeringUserLabel = client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + "app.kubernetes.io/component": "peering-user", + }), +} + +var minimumClusterPermissions = []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "create"}, + }, + { + APIGroups: []string{"ipam.liqo.io"}, + Resources: []string{"networks"}, + Verbs: []string{"get", "list"}, + }, + { + APIGroups: []string{"networking.liqo.io"}, + Resources: []string{"configurations", "gatewayclients", "gatewayservers", "publickeies"}, + Verbs: []string{"get", "list"}, + }, + { + APIGroups: []string{"core.liqo.io"}, + Resources: []string{"foreignclusters"}, + Verbs: []string{"get", "list"}, + }, + { + APIGroups: []string{"authentication.liqo.io"}, + Resources: []string{"tenants"}, + Verbs: []string{"create", "list"}, + }, +} + +// EnsureRoles ensures that the required roles are created and bound to the user. +func EnsureRoles(ctx context.Context, c client.Client, clusterID liqov1beta1.ClusterID, userCN, tenantNsName string) error { + if err := ensureLiqoNsReaderRole(ctx, c, userCN, clusterID); err != nil { + return err + } + + if err := ensureTenantNsWriterRole(ctx, c, userCN, clusterID, tenantNsName); err != nil { + return err + } + + if err := ensureClusterMinPermissions(ctx, c, userCN, clusterID); err != nil { + return err + } + + return nil +} + +// IsExistingPeerUser checks whether the user has already been created. +func IsExistingPeerUser(ctx context.Context, c client.Client, clusterID liqov1beta1.ClusterID) (bool, error) { + userName := GetUserNameFromClusterID(clusterID) + + clusterRoleList := &rbacv1.ClusterRoleList{} + if err := c.List(ctx, clusterRoleList, &client.ListOptions{ + LabelSelector: getUserLabelSelector(userName), + }); err != nil { + return false, fmt.Errorf("unable to check whether the user has already been created: %w", err) + } + + return len(clusterRoleList.Items) > 0, nil +} + +// RemovePermissions removes the permissions related to the user. +func RemovePermissions(ctx context.Context, c client.Client, clusterID liqov1beta1.ClusterID) error { + userName := GetUserNameFromClusterID(clusterID) + + userLabelSelector := getUserLabelSelector(userName) + + // Delete the ClusterRole related to the user + if err := c.DeleteAllOf(ctx, &rbacv1.ClusterRole{}, client.MatchingLabelsSelector{Selector: userLabelSelector}); err != nil { + return fmt.Errorf("unable to delete ClusterRoles: %w", err) + } + + // Delete the ClusterRoleBindings related to the user + if err := c.DeleteAllOf(ctx, &rbacv1.ClusterRoleBinding{}, client.MatchingLabelsSelector{Selector: userLabelSelector}); err != nil { + return fmt.Errorf("unable to delete ClusterRoleBindings: %w", err) + } + + // Cannot delete RoleBinding with DeleteAllOf, list it and delete one by one + roleBindingList := &rbacv1.RoleBindingList{} + + if err := c.List(ctx, roleBindingList, &client.ListOptions{ + LabelSelector: getUserLabelSelector(userName), + }); err != nil { + return fmt.Errorf("unable to get RoleBindings: %w", err) + } + + for i := range roleBindingList.Items { + if err := c.Delete(ctx, &roleBindingList.Items[i]); err != nil { + return fmt.Errorf("unable to delete RoleBinding %q: %w", roleBindingList.Items[i].Name, err) + } + } + + // Delete the CertificateSigningRequest with the certificate of the user + if err := c.DeleteAllOf( + ctx, + &certv1.CertificateSigningRequest{}, + client.MatchingLabelsSelector{Selector: userLabelSelector}, + ); err != nil { + return fmt.Errorf("unable to delete ClusterRoleBindings: %w", err) + } + + return nil +} + +func getUserLabelSelector(userName string) labels.Selector { + return labels.SelectorFromSet(labels.Set{ + consts.PeeringUserNameLabelKey: userName, + }) +} + +// ensureLiqoNsReaderRole ensures that the peering-user Role is bound to the user in the Liqo namespace. +func ensureLiqoNsReaderRole(ctx context.Context, c client.Client, userCN string, clusterID liqov1beta1.ClusterID) error { + var peeringUserRoleList rbacv1.RoleList + if err := c.List(ctx, &peeringUserRoleList, &peeringUserLabel); err != nil { + return fmt.Errorf("unable to get peering-user Role from liqo namespace: %w", err) + } + + if nRoles := len(peeringUserRoleList.Items); nRoles == 0 { + return fmt.Errorf("no peering-user Role found in the Liqo namespace") + } else if nRoles > 1 { + return fmt.Errorf("multiple peering-user Roles found in the Liqo namespace") + } + + peeringUserRole := peeringUserRoleList.Items[0] + userName := GetUserNameFromClusterID(clusterID) + + // Bind the roles to operate on the liqo namespace + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-liqo-ns-reader", userName), + Namespace: peeringUserRole.Namespace, + Labels: map[string]string{ + consts.PeeringUserNameLabelKey: userName, + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: userCN, + APIGroup: "rbac.authorization.k8s.io", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: peeringUserRole.Name, + }, + } + + if err := c.Create(ctx, roleBinding); err != nil { + return fmt.Errorf("unable to create role binding in the %q namespace: %w", peeringUserRole.Namespace, err) + } + + return nil +} + +func ensureTenantNsWriterRole(ctx context.Context, c client.Client, userCN string, clusterID liqov1beta1.ClusterID, tenantNsName string) error { + var peeringClusterRoles rbacv1.ClusterRoleList + if err := c.List(ctx, &peeringClusterRoles, &peeringUserLabel); err != nil { + return fmt.Errorf("unable to get peering-user role from liqo namespace: %w", err) + } + + if nRoles := len(peeringClusterRoles.Items); nRoles == 0 { + return fmt.Errorf("no peering-user ClusterRole found") + } else if nRoles > 1 { + return fmt.Errorf("multiple peering-user ClusterRoles found ") + } + + // bind the ClusterRole to the userName user + userName := GetUserNameFromClusterID(clusterID) + peeringUserClusterRole := peeringClusterRoles.Items[0] + clusterRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-tenant-ns-writer", userName), + Namespace: tenantNsName, + Labels: map[string]string{ + consts.PeeringUserNameLabelKey: userName, + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: userCN, + APIGroup: "rbac.authorization.k8s.io", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: peeringUserClusterRole.Name, + }, + } + + if err := c.Create(ctx, clusterRoleBinding); err != nil { + return fmt.Errorf("unable to create cluster role binding: %w", err) + } + + return nil +} + +func ensureClusterMinPermissions(ctx context.Context, c client.Client, userCN string, clusterID liqov1beta1.ClusterID) error { + userName := GetUserNameFromClusterID(clusterID) + + // Append to the minimum permissions the permissions to operate on the user Tenant resource + permissions := append( + []rbacv1.PolicyRule{ + { + APIGroups: []string{"authentication.liqo.io"}, + Resources: []string{"tenants"}, + ResourceNames: []string{string(clusterID)}, + Verbs: []string{"update", "get", "delete"}, + }, + }, + minimumClusterPermissions..., + ) + + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-cluster-min-perm", userName), + Labels: map[string]string{ + consts.PeeringUserNameLabelKey: userName, + }, + }, + Rules: permissions, + } + + if err := c.Create(ctx, clusterRole); err != nil { + return fmt.Errorf("unable to create ClusterRole for minimum permissions on the cluster: %w", err) + } + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-cluster-min-perm", userName), + Labels: map[string]string{ + consts.PeeringUserNameLabelKey: userName, + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: userCN, + APIGroup: "rbac.authorization.k8s.io", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: clusterRole.Name, + }, + } + + if err := c.Create(ctx, clusterRoleBinding); err != nil { + return fmt.Errorf("unable to create ClusterRoleBinding for minimum permissions on the cluster: %w", err) + } + + return nil +} diff --git a/pkg/liqoctl/rest/tenant/generate.go b/pkg/liqoctl/rest/tenant/generate.go index cd3cfc26a7..c34e025bb7 100644 --- a/pkg/liqoctl/rest/tenant/generate.go +++ b/pkg/liqoctl/rest/tenant/generate.go @@ -35,7 +35,7 @@ const liqoctlGenerateConfigHelp = `Generate the Tenant resource to be applied on This commands generates a Tenant filled with all the authentication parameters needed to authenticate with the remote cluster. It signs the nonce provided by the remote cluster and generates the CSR. -The Nonce can be provided as a flag or it can be retrieved from the secret in the tenant namespace (if existing). +The Nonce can be provided as a flag or it can be retrieved from the secret in the tenant namespace (if existing). Examples: $ {{ .Executable }} generate tenant --remote-cluster-id remote-cluster-id` @@ -98,13 +98,13 @@ func (o *Options) handleGenerate(ctx context.Context) error { } // Wait for secret to be filled with the signed nonce. - if err := waiter.ForSignedNonce(ctx, o.remoteClusterID.GetClusterID(), true); err != nil { + if err := waiter.ForSignedNonce(ctx, o.remoteClusterID.GetClusterID(), true, tenantNs.GetName()); err != nil { opts.Printer.CheckErr(fmt.Errorf("unable to wait for nonce to be signed: %w", err)) return err } // Retrieve signed nonce from secret. - signedNonce, err := authutils.RetrieveSignedNonce(ctx, opts.CRClient, o.remoteClusterID.GetClusterID()) + signedNonce, err := authutils.RetrieveSignedNonce(ctx, opts.CRClient, o.remoteClusterID.GetClusterID(), tenantNs.GetName()) if err != nil { opts.Printer.CheckErr(fmt.Errorf("unable to retrieve signed nonce: %w", err)) return err diff --git a/pkg/liqoctl/unpeer/handler.go b/pkg/liqoctl/unpeer/handler.go index 0e34e28d1f..3092f70855 100644 --- a/pkg/liqoctl/unpeer/handler.go +++ b/pkg/liqoctl/unpeer/handler.go @@ -35,9 +35,9 @@ type Options struct { RemoteFactory *factory.Factory waiter *wait.Waiter - Timeout time.Duration - Wait bool - KeepNamespaces bool + Timeout time.Duration + Wait bool + DeleteNamespace bool consumerClusterID liqov1beta1.ClusterID providerClusterID liqov1beta1.ClusterID @@ -85,8 +85,8 @@ func (o *Options) RunUnpeer(ctx context.Context) error { o.LocalFactory.Printer.CheckErr(fmt.Errorf("an error occurred while checking bidirectional peering: %v", output.PrettyErr(err))) return err } - if bidirectional && !o.KeepNamespaces { - err = fmt.Errorf("cannot unpeer bidirectional peering without keeping namespaces, please set the --keep-namespaces flag") + if bidirectional && o.DeleteNamespace { + err = fmt.Errorf("cannot delete the tenant namespace when a bidirectional is enabled, please remote the --delete-namespaces flag") o.LocalFactory.Printer.CheckErr(err) return err } @@ -111,7 +111,7 @@ func (o *Options) RunUnpeer(ctx context.Context) error { } } - if !o.KeepNamespaces { + if o.DeleteNamespace { consumer := unauthenticate.NewCluster(o.LocalFactory) provider := unauthenticate.NewCluster(o.RemoteFactory) diff --git a/pkg/liqoctl/utils/utils.go b/pkg/liqoctl/utils/utils.go index e1015f0c16..b25e76864c 100644 --- a/pkg/liqoctl/utils/utils.go +++ b/pkg/liqoctl/utils/utils.go @@ -21,11 +21,26 @@ import ( "strings" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/liqotech/liqo/pkg/consts" liqolabels "github.com/liqotech/liqo/pkg/utils/labels" ) +// GetCtrlManagerContainer retrieves the container of the controller manager from the deployment. +func GetCtrlManagerContainer(ctrlDeployment *appsv1.Deployment) (*corev1.Container, error) { + // Get the container of the controller manager + containers := ctrlDeployment.Spec.Template.Spec.Containers + for i := range containers { + if containers[i].Name == consts.ControllerManagerAppName { + return &containers[i], nil + } + } + + return nil, fmt.Errorf("invalid controller manager deployment: no container with name %q found", consts.ControllerManagerAppName) +} + // RetrieveLiqoControllerManagerDeploymentArgs retrieves the list of arguments associated with the liqo controller manager deployment. func RetrieveLiqoControllerManagerDeploymentArgs(ctx context.Context, cl client.Client, namespace string) ([]string, error) { // Retrieve the deployment of the liqo controller manager component diff --git a/pkg/liqoctl/wait/wait.go b/pkg/liqoctl/wait/wait.go index ab6e6163f1..d558559b20 100644 --- a/pkg/liqoctl/wait/wait.go +++ b/pkg/liqoctl/wait/wait.go @@ -333,7 +333,7 @@ func (w *Waiter) ForConnectionEstablished(ctx context.Context, conn *networkingv } // ForNonce waits until the secret containing the nonce has been created or the timeout expires. -func (w *Waiter) ForNonce(ctx context.Context, remoteClusterID liqov1beta1.ClusterID, silent bool) error { +func (w *Waiter) ForNonce(ctx context.Context, remoteClusterID liqov1beta1.ClusterID, tenantNamespace string, silent bool) error { var s *pterm.SpinnerPrinter if !silent { @@ -341,7 +341,7 @@ func (w *Waiter) ForNonce(ctx context.Context, remoteClusterID liqov1beta1.Clust } err := wait.PollUntilContextCancel(ctx, 1*time.Second, true, func(ctx context.Context) (done bool, err error) { - secret, err := getters.GetNonceSecretByClusterID(ctx, w.CRClient, remoteClusterID) + secret, err := getters.GetNonceSecretByClusterID(ctx, w.CRClient, remoteClusterID, tenantNamespace) if err != nil { return false, client.IgnoreNotFound(err) } @@ -366,7 +366,7 @@ func (w *Waiter) ForNonce(ctx context.Context, remoteClusterID liqov1beta1.Clust } // ForSignedNonce waits until the signed nonce secret has been signed and returns the signature. -func (w *Waiter) ForSignedNonce(ctx context.Context, remoteClusterID liqov1beta1.ClusterID, silent bool) error { +func (w *Waiter) ForSignedNonce(ctx context.Context, remoteClusterID liqov1beta1.ClusterID, silent bool, tenantNs string) error { var s *pterm.SpinnerPrinter if !silent { @@ -374,7 +374,7 @@ func (w *Waiter) ForSignedNonce(ctx context.Context, remoteClusterID liqov1beta1 } err := wait.PollUntilContextCancel(ctx, 1*time.Second, true, func(ctx context.Context) (done bool, err error) { - secret, err := getters.GetSignedNonceSecretByClusterID(ctx, w.CRClient, remoteClusterID) + secret, err := getters.GetSignedNonceSecretByClusterID(ctx, w.CRClient, remoteClusterID, tenantNs) if err != nil { return false, client.IgnoreNotFound(err) } diff --git a/pkg/utils/getters/k8sGetters.go b/pkg/utils/getters/k8sGetters.go index 3b8169018e..2db17ef358 100644 --- a/pkg/utils/getters/k8sGetters.go +++ b/pkg/utils/getters/k8sGetters.go @@ -215,9 +215,11 @@ func ListNodesByClusterID(ctx context.Context, cl client.Client, clusterID liqov } // GetNonceSecretByClusterID returns the secret containing the nonce to be signed by the consumer cluster. -func GetNonceSecretByClusterID(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID) (*corev1.Secret, error) { +func GetNonceSecretByClusterID(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID, + tenantNs string) (*corev1.Secret, error) { var secrets corev1.SecretList if err := cl.List(ctx, &secrets, &client.ListOptions{ + Namespace: tenantNs, LabelSelector: labels.SelectorFromSet(map[string]string{ consts.RemoteClusterID: string(remoteClusterID), consts.NonceSecretLabelKey: "true", @@ -237,12 +239,16 @@ func GetNonceSecretByClusterID(ctx context.Context, cl client.Client, remoteClus } // GetSignedNonceSecretByClusterID returns the secret containing the nonce signed by the consumer cluster. -func GetSignedNonceSecretByClusterID(ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID) (*corev1.Secret, error) { +func GetSignedNonceSecretByClusterID( + ctx context.Context, cl client.Client, remoteClusterID liqov1beta1.ClusterID, tentantNs string) (*corev1.Secret, error) { var secrets corev1.SecretList - if err := cl.List(ctx, &secrets, client.MatchingLabels{ - consts.RemoteClusterID: string(remoteClusterID), - consts.SignedNonceSecretLabelKey: "true", - }); err != nil { + if err := cl.List(ctx, &secrets, + client.MatchingLabels{ + consts.RemoteClusterID: string(remoteClusterID), + consts.SignedNonceSecretLabelKey: "true", + }, + &client.ListOptions{Namespace: tentantNs}, + ); err != nil { return nil, err } diff --git a/test/e2e/pipeline/installer/liqoctl/peer.sh b/test/e2e/pipeline/installer/liqoctl/peer.sh index e5670f00d4..1cd7c761f5 100755 --- a/test/e2e/pipeline/installer/liqoctl/peer.sh +++ b/test/e2e/pipeline/installer/liqoctl/peer.sh @@ -26,13 +26,24 @@ error() { } trap 'error "${BASH_SOURCE}" "${LINENO}"' ERR +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +# shellcheck disable=SC1091 +# shellcheck source=../../utils.sh +source "${SCRIPT_DIR}/../../utils.sh" + +mkdir -p "${TMPDIR}/kubeconfigs/generated" +CLUSTER_ID=$(forge_clustername 1) for i in $(seq 2 "${CLUSTER_NUMBER}") do export KUBECONFIG="${TMPDIR}/kubeconfigs/liqo_kubeconf_1" - export PROVIDER_KUBECONFIG="${TMPDIR}/kubeconfigs/liqo_kubeconf_${i}" + export PROVIDER_KUBECONFIG_ADMIN="${TMPDIR}/kubeconfigs/liqo_kubeconf_${i}" + + echo "Generating kubeconfig for consumer cluster ${i}" + "${LIQOCTL}" generate peering-user --kubeconfig "${PROVIDER_KUBECONFIG_ADMIN}" --consumer-cluster-id "${CLUSTER_ID}" > "${TMPDIR}/kubeconfigs/generated/liqo_kubeconf_${i}" + PROVIDER_KUBECONFIG="${TMPDIR}/kubeconfigs/generated/liqo_kubeconf_${i}" ARGS=(--kubeconfig "${KUBECONFIG}" --remote-kubeconfig "${PROVIDER_KUBECONFIG}") - + if [[ "${INFRA}" == "cluster-api" ]]; then ARGS=("${ARGS[@]}" --server-service-type NodePort) elif [[ "${INFRA}" == "kind" ]]; then @@ -52,7 +63,7 @@ do ARGS=("${ARGS[@]}") "${LIQOCTL}" peer "${ARGS[@]}" - + # Sleep a bit, to avoid generating a race condition with the # authentication process triggered by the incoming peering. sleep 1 diff --git a/test/e2e/pipeline/installer/liqoctl/unpeer.sh b/test/e2e/pipeline/installer/liqoctl/unpeer.sh index db804ecbe8..187b734f53 100755 --- a/test/e2e/pipeline/installer/liqoctl/unpeer.sh +++ b/test/e2e/pipeline/installer/liqoctl/unpeer.sh @@ -39,14 +39,21 @@ error() { } trap 'error "${BASH_SOURCE}" "${LINENO}"' ERR -CONSUMER_KUBECONFIG="${TMPDIR}/kubeconfigs/liqo_kubeconf_1" +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +# shellcheck disable=SC1091 +# shellcheck source=../../utils.sh +source "${SCRIPT_DIR}/../../utils.sh" +CONSUMER_KUBECONFIG="${TMPDIR}/kubeconfigs/liqo_kubeconf_1" +CLUSTER_ID=$(forge_clustername 1) for i in $(seq 2 "${CLUSTER_NUMBER}"); do export KUBECONFIG="${CONSUMER_KUBECONFIG}" - export PROVIDER_KUBECONFIG="${TMPDIR}/kubeconfigs/liqo_kubeconf_${i}" + export PROVIDER_KUBECONFIG_ADMIN="${TMPDIR}/kubeconfigs/liqo_kubeconf_${i}" + export PROVIDER_KUBECONFIG="${TMPDIR}/kubeconfigs/generated/liqo_kubeconf_${i}" "${LIQOCTL}" unpeer --kubeconfig "${KUBECONFIG}" --remote-kubeconfig "${PROVIDER_KUBECONFIG}" --skip-confirm + "${LIQOCTL}" delete peering-user --kubeconfig "${PROVIDER_KUBECONFIG_ADMIN}" --consumer-cluster-id "${CLUSTER_ID}" done; # check that the peering is correctly removed