From 6ea7c08e696acd0b62925536e1a704a15bc57850 Mon Sep 17 00:00:00 2001 From: Alessandro Olivero Date: Wed, 13 Dec 2023 10:45:12 +0100 Subject: [PATCH] liqoctl: add identity command --- cmd/liqoctl/cmd/generate.go | 1 + cmd/liqoctl/cmd/root.go | 4 + pkg/identityManager/certificate.go | 41 +++-- .../certificateIdentityProvider.go | 17 +- pkg/identityManager/client.go | 6 +- pkg/identityManager/fake/reader.go | 5 + pkg/identityManager/identityManager_test.go | 4 +- pkg/identityManager/interface.go | 3 + .../virtualnode-controller/drain.go | 63 +------ pkg/liqoctl/rest/identity/create.go | 56 ++++++ pkg/liqoctl/rest/identity/delete.go | 28 +++ pkg/liqoctl/rest/identity/generate.go | 174 ++++++++++++++++++ pkg/liqoctl/rest/identity/get.go | 28 +++ pkg/liqoctl/rest/identity/types.go | 48 +++++ pkg/liqoctl/rest/identity/update.go | 156 ++++++++++++++++ pkg/liqoctl/rest/types.go | 1 + pkg/liqoctl/update/doc.go | 16 ++ pkg/liqoctl/update/types.go | 52 ++++++ pkg/utils/deployment.go | 81 ++++++++ 19 files changed, 702 insertions(+), 82 deletions(-) create mode 100644 pkg/liqoctl/rest/identity/create.go create mode 100644 pkg/liqoctl/rest/identity/delete.go create mode 100644 pkg/liqoctl/rest/identity/generate.go create mode 100644 pkg/liqoctl/rest/identity/get.go create mode 100644 pkg/liqoctl/rest/identity/types.go create mode 100644 pkg/liqoctl/rest/identity/update.go create mode 100644 pkg/liqoctl/update/doc.go create mode 100644 pkg/liqoctl/update/types.go create mode 100644 pkg/utils/deployment.go diff --git a/cmd/liqoctl/cmd/generate.go b/cmd/liqoctl/cmd/generate.go index 5e63ecb538..550f8a7f94 100644 --- a/cmd/liqoctl/cmd/generate.go +++ b/cmd/liqoctl/cmd/generate.go @@ -53,6 +53,7 @@ func newGenerateCommand(ctx context.Context, f *factory.Factory) *cobra.Command options := &rest.GenerateOptions{ Factory: f, + Liqoctl: liqoctl, } for _, r := range liqoResources { diff --git a/cmd/liqoctl/cmd/root.go b/cmd/liqoctl/cmd/root.go index d8f9d298d1..0671d95a9e 100644 --- a/cmd/liqoctl/cmd/root.go +++ b/cmd/liqoctl/cmd/root.go @@ -38,8 +38,10 @@ import ( "github.com/liqotech/liqo/pkg/liqoctl/rest/configuration" "github.com/liqotech/liqo/pkg/liqoctl/rest/gatewayclient" "github.com/liqotech/liqo/pkg/liqoctl/rest/gatewayserver" + "github.com/liqotech/liqo/pkg/liqoctl/rest/identity" "github.com/liqotech/liqo/pkg/liqoctl/rest/publickey" "github.com/liqotech/liqo/pkg/liqoctl/rest/virtualnode" + "github.com/liqotech/liqo/pkg/liqoctl/update" ) var liqoctl string @@ -50,6 +52,7 @@ var liqoResources = []rest.APIProvider{ gatewayserver.GatewayServer, gatewayclient.GatewayClient, publickey.PublicKey, + identity.Identity, } func init() { @@ -138,6 +141,7 @@ func NewRootCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(get.NewGetCommand(ctx, liqoResources, f)) cmd.AddCommand(create.NewCreateCommand(ctx, liqoResources, f)) cmd.AddCommand(delete.NewDeleteCommand(ctx, liqoResources, f)) + cmd.AddCommand(update.NewUpdateCommand(ctx, liqoResources, f)) return cmd } diff --git a/pkg/identityManager/certificate.go b/pkg/identityManager/certificate.go index eb41762d76..feec1d0378 100644 --- a/pkg/identityManager/certificate.go +++ b/pkg/identityManager/certificate.go @@ -22,7 +22,7 @@ import ( "strconv" "time" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -34,10 +34,14 @@ import ( "github.com/liqotech/liqo/pkg/discovery" ) -// StoreIdentity stores the identity to authenticate with a remote cluster. -func (certManager *identityManager) StoreIdentity(ctx context.Context, remoteCluster discoveryv1alpha1.ClusterIdentity, - namespace string, key []byte, remoteProxyURL string, identityResponse *auth.CertificateIdentityResponse) error { - secret := &v1.Secret{ +// GenerateIdentitySecret generates the identity secret to authenticate with a remote cluster. +func (certManager *identityManager) GenerateIdentitySecret(ctx context.Context, remoteCluster discoveryv1alpha1.ClusterIdentity, + namespace string, key []byte, remoteProxyURL string, identityResponse *auth.CertificateIdentityResponse) (*corev1.Secret, error) { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: corev1.SchemeGroupVersion.String(), + }, ObjectMeta: metav1.ObjectMeta{ GenerateName: identitySecretRoot + "-", Namespace: namespace, @@ -69,7 +73,7 @@ func (certManager *identityManager) StoreIdentity(ctx context.Context, remoteClu } else { certificate, err := base64.StdEncoding.DecodeString(identityResponse.Certificate) if err != nil { - return fmt.Errorf("failed to decode certificate: %w", err) + return nil, fmt.Errorf("failed to decode certificate: %w", err) } secret.Data[certificateSecretKey] = certificate @@ -79,7 +83,7 @@ func (certManager *identityManager) StoreIdentity(ctx context.Context, remoteClu if identityResponse.APIServerCA != "" { apiServerCa, err := base64.StdEncoding.DecodeString(identityResponse.APIServerCA) if err != nil { - return fmt.Errorf("failed to decode certification authority: %w", err) + return nil, fmt.Errorf("failed to decode certification authority: %w", err) } secret.Data[apiServerCaSecretKey] = apiServerCa @@ -89,14 +93,25 @@ func (certManager *identityManager) StoreIdentity(ctx context.Context, remoteClu secret.StringData[apiProxyURLSecretKey] = remoteProxyURL } - if _, err := certManager.client.CoreV1().Secrets(secret.Namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil { + return secret, nil +} + +// StoreIdentity generates and stores the identity to authenticate with a remote cluster. +func (certManager *identityManager) StoreIdentity(ctx context.Context, remoteCluster discoveryv1alpha1.ClusterIdentity, + namespace string, key []byte, remoteProxyURL string, identityResponse *auth.CertificateIdentityResponse) error { + secret, err := certManager.GenerateIdentitySecret(ctx, remoteCluster, namespace, key, remoteProxyURL, identityResponse) + if err != nil { + return err + } + + if _, err = certManager.client.CoreV1().Secrets(secret.Namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil { return fmt.Errorf("failed to create secret: %w", err) } return nil } -// getSecret retrieves the identity secret given the clusterID. -func (certManager *identityManager) getSecret(remoteCluster discoveryv1alpha1.ClusterIdentity) (*v1.Secret, error) { +// GetSecret retrieves the identity secret given the clusterID. +func (certManager *identityManager) GetSecret(remoteCluster discoveryv1alpha1.ClusterIdentity) (*corev1.Secret, error) { namespace, err := certManager.namespaceManager.GetNamespace(context.TODO(), remoteCluster) if err != nil { return nil, err @@ -107,7 +122,7 @@ func (certManager *identityManager) getSecret(remoteCluster discoveryv1alpha1.Cl // getSecretInNamespace retrieves the identity secret in the given Namespace. func (certManager *identityManager) getSecretInNamespace(remoteCluster discoveryv1alpha1.ClusterIdentity, - namespace string) (*v1.Secret, error) { + namespace string) (*corev1.Secret, error) { labelSelector := metav1.LabelSelector{ MatchLabels: map[string]string{ localIdentitySecretLabel: "true", @@ -142,7 +157,7 @@ func (certManager *identityManager) getSecretInNamespace(remoteCluster discovery } // getExpireTime reads the expire time from the annotations of the secret. -func getExpireTime(secret *v1.Secret) int64 { +func getExpireTime(secret *corev1.Secret) int64 { now := time.Now().Unix() if secret.Annotations == nil { klog.Warningf("annotation %v not found in secret %v/%v", certificateExpireTimeAnnotation, secret.Namespace, secret.Name) @@ -163,7 +178,7 @@ func getExpireTime(secret *v1.Secret) int64 { return n } -func (certManager *identityManager) isAwsIdentity(secret *v1.Secret) bool { +func (certManager *identityManager) isAwsIdentity(secret *corev1.Secret) bool { data := secret.Data keys := []string{awsAccessKeyIDSecretKey, awsSecretAccessKeySecretKey, awsRegionSecretKey, awsEKSClusterIDSecretKey, awsIAMUserArnSecretKey} for i := range keys { diff --git a/pkg/identityManager/certificateIdentityProvider.go b/pkg/identityManager/certificateIdentityProvider.go index 9ed197219c..9d90649523 100644 --- a/pkg/identityManager/certificateIdentityProvider.go +++ b/pkg/identityManager/certificateIdentityProvider.go @@ -181,10 +181,21 @@ func (identityProvider *certificateIdentityProvider) storeRemoteCertificate(clus }, } - if secret, err = identityProvider.client.CoreV1(). - Secrets(namespace.Name).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil { + res, err := identityProvider.client.CoreV1(). + Secrets(namespace.Name).Create(context.TODO(), secret, metav1.CreateOptions{}) + switch { + case kerrors.IsAlreadyExists(err): + res, err = identityProvider.client.CoreV1(). + Secrets(namespace.Name).Update(context.TODO(), secret, metav1.UpdateOptions{}) + if err != nil { + klog.Error(err) + return nil, err + } + return res, nil + case err != nil: klog.Error(err) return nil, err + default: + return res, nil } - return secret, nil } diff --git a/pkg/identityManager/client.go b/pkg/identityManager/client.go index 0c8860ecfb..3d88fa0006 100644 --- a/pkg/identityManager/client.go +++ b/pkg/identityManager/client.go @@ -36,7 +36,7 @@ func (certManager *identityManager) GetConfig(remoteCluster discoveryv1alpha1.Cl var err error if namespace == "" { - secret, err = certManager.getSecret(remoteCluster) + secret, err = certManager.GetSecret(remoteCluster) } else { secret, err = certManager.getSecretInNamespace(remoteCluster, namespace) } @@ -57,7 +57,7 @@ func (certManager *identityManager) GetSecretNamespacedName(remoteCluster discov var err error if namespace == "" { - secret, err = certManager.getSecret(remoteCluster) + secret, err = certManager.GetSecret(remoteCluster) } else { secret, err = certManager.getSecretInNamespace(remoteCluster, namespace) } @@ -88,7 +88,7 @@ func (certManager *identityManager) GetRemoteTenantNamespace(remoteCluster disco var err error if localTenantNamespaceName == "" { - secret, err = certManager.getSecret(remoteCluster) + secret, err = certManager.GetSecret(remoteCluster) } else { secret, err = certManager.getSecretInNamespace(remoteCluster, localTenantNamespaceName) } diff --git a/pkg/identityManager/fake/reader.go b/pkg/identityManager/fake/reader.go index 4ea621180d..298b3b4e33 100644 --- a/pkg/identityManager/fake/reader.go +++ b/pkg/identityManager/fake/reader.go @@ -86,3 +86,8 @@ func (i *IdentityReader) GetRemoteTenantNamespace(remoteCluster discoveryv1alpha } return "", fmt.Errorf("remote cluster ID %v not found", remoteCluster.ClusterID) } + +// GetSecret retrieves the secret associated with a remote cluster. +func (i *IdentityReader) GetSecret(remoteCluster discoveryv1alpha1.ClusterIdentity) (*corev1.Secret, error) { + panic("implement me") +} diff --git a/pkg/identityManager/identityManager_test.go b/pkg/identityManager/identityManager_test.go index 2d1c85b558..3ca607aa3c 100644 --- a/pkg/identityManager/identityManager_test.go +++ b/pkg/identityManager/identityManager_test.go @@ -126,7 +126,7 @@ var _ = Describe("IdentityManager", func() { idMan, ok := identityMan.(*identityManager) Expect(ok).To(BeTrue()) - secret, err := idMan.getSecret(remoteCluster) + secret, err := idMan.GetSecret(remoteCluster) Expect(err).To(Succeed()) commonSecretChecks(secret) @@ -155,7 +155,7 @@ var _ = Describe("IdentityManager", func() { } idMan.iamTokenManager = &tokenManager - secret, err := idMan.getSecret(remoteCluster) + secret, err := idMan.GetSecret(remoteCluster) Expect(err).To(Succeed()) commonSecretChecks(secret) diff --git a/pkg/identityManager/interface.go b/pkg/identityManager/interface.go index 891e5a7997..3bd2cce321 100644 --- a/pkg/identityManager/interface.go +++ b/pkg/identityManager/interface.go @@ -32,6 +32,7 @@ type IdentityReader interface { GetConfigFromSecret(secret *corev1.Secret) (*rest.Config, error) GetRemoteTenantNamespace(remoteCluster discoveryv1alpha1.ClusterIdentity, namespace string) (string, error) GetSecretNamespacedName(remoteCluster discoveryv1alpha1.ClusterIdentity, namespace string) (types.NamespacedName, error) + GetSecret(remoteCluster discoveryv1alpha1.ClusterIdentity) (*corev1.Secret, error) } // IdentityManager interface provides the methods to manage identities for the remote clusters. @@ -40,6 +41,8 @@ type IdentityManager interface { StoreIdentity(ctx context.Context, remoteCluster discoveryv1alpha1.ClusterIdentity, namespace string, key []byte, remoteProxyURL string, identityResponse *auth.CertificateIdentityResponse) error + GenerateIdentitySecret(ctx context.Context, remoteCluster discoveryv1alpha1.ClusterIdentity, + namespace string, key []byte, remoteProxyURL string, identityResponse *auth.CertificateIdentityResponse) (*corev1.Secret, error) } // IdentityProvider provides the interface to retrieve and approve remote cluster identities. diff --git a/pkg/liqo-controller-manager/virtualnode-controller/drain.go b/pkg/liqo-controller-manager/virtualnode-controller/drain.go index b74c6148ef..cc6761e87a 100644 --- a/pkg/liqo-controller-manager/virtualnode-controller/drain.go +++ b/pkg/liqo-controller-manager/virtualnode-controller/drain.go @@ -19,15 +19,12 @@ import ( "time" corev1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" virtualkubeletv1alpha1 "github.com/liqotech/liqo/apis/virtualkubelet/v1alpha1" + "github.com/liqotech/liqo/pkg/utils" "github.com/liqotech/liqo/pkg/utils/indexer" ) @@ -44,7 +41,7 @@ func drainNode(ctx context.Context, cl client.Client, vn *virtualkubeletv1alpha1 return err } - if err = evictPods(ctx, cl, podsToEvict); err != nil { + if err = utils.EvictPods(ctx, cl, podsToEvict, waitForPodTerminationCheckPeriod); err != nil { klog.Error(err) return err } @@ -69,59 +66,3 @@ func getPodsForDeletion(ctx context.Context, cl client.Client, vn *virtualkubele } return podList, nil } - -// evictPods performs the eviction of the provided list of pods in parallel, waiting for their deletion. -func evictPods(ctx context.Context, cl client.Client, podList *corev1.PodList) error { - for i := range podList.Items { - if err := evictPod(ctx, cl, &podList.Items[i]); err != nil { - return err - } - } - - for i := range podList.Items { - if err := waitPodForDelete(ctx, cl, &podList.Items[i]); err != nil { - return err - } - } - - return nil -} - -// evictPod evicts the provided pod and waits for its deletion. -func evictPod(ctx context.Context, cl client.Client, pod *corev1.Pod) error { - eviction := &policyv1.Eviction{ - ObjectMeta: metav1.ObjectMeta{ - Name: pod.Name, - Namespace: pod.Namespace, - }, - DeleteOptions: &metav1.DeleteOptions{}, - } - - if err := cl.SubResource("eviction").Create(ctx, pod, eviction); err != nil { - return err - } - - klog.V(4).Infof("Drain node %s -> pod %v/%v eviction started", pod.Spec.NodeName, pod.Namespace, pod.Name) - - return nil -} - -// waitForDelete waits for the pod deletion. -func waitPodForDelete(ctx context.Context, cl client.Client, pod *corev1.Pod) error { - //nolint:staticcheck // Waiting for PollWithContextCancel implementation. - return wait.PollImmediateInfinite(waitForPodTerminationCheckPeriod, func() (bool, error) { - klog.Infof("Drain node %s -> pod %v/%v waiting for deletion", pod.Spec.NodeName, pod.Namespace, pod.Name) - updatedPod := &corev1.Pod{} - err := cl.Get(ctx, client.ObjectKey{Namespace: pod.Namespace, Name: pod.Name}, updatedPod) - if kerrors.IsNotFound(err) || (updatedPod != nil && - pod.ObjectMeta.UID != updatedPod.ObjectMeta.UID) { - klog.Infof("Drain node %s -> pod %v/%v successfully deleted", pod.Spec.NodeName, pod.Namespace, pod.Name) - return true, nil - } - if err != nil { - klog.Error(err) - return false, err - } - return false, nil - }) -} diff --git a/pkg/liqoctl/rest/identity/create.go b/pkg/liqoctl/rest/identity/create.go new file mode 100644 index 0000000000..0a1f11c5c5 --- /dev/null +++ b/pkg/liqoctl/rest/identity/create.go @@ -0,0 +1,56 @@ +// 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 identity + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/printers" + + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// Create creates a VirtualNode. +func (o *Options) Create(_ context.Context, _ *rest.CreateOptions) *cobra.Command { + panic("not implemented") +} + +// output implements the logic to output the generated Configuration resource. +func (o *Options) output(conf *corev1.Secret) error { + var outputFormat string + switch { + //case o.createOptions != nil: + // outputFormat = o.createOptions.OutputFormat + case o.generateOptions != nil: + outputFormat = o.generateOptions.OutputFormat + default: + return fmt.Errorf("unable to determine output format") + } + var printer printers.ResourcePrinter + switch outputFormat { + case "yaml": + printer = &printers.YAMLPrinter{} + case "json": + printer = &printers.JSONPrinter{} + default: + return fmt.Errorf("unsupported output format %q", outputFormat) + } + + return printer.PrintObj(conf, os.Stdout) +} diff --git a/pkg/liqoctl/rest/identity/delete.go b/pkg/liqoctl/rest/identity/delete.go new file mode 100644 index 0000000000..ed9c1fdda2 --- /dev/null +++ b/pkg/liqoctl/rest/identity/delete.go @@ -0,0 +1,28 @@ +// 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 identity + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// Delete deletes a virtual node. +func (o *Options) Delete(_ context.Context, _ *rest.DeleteOptions) *cobra.Command { + panic("not implemented") +} diff --git a/pkg/liqoctl/rest/identity/generate.go b/pkg/liqoctl/rest/identity/generate.go new file mode 100644 index 0000000000..ea0c2cb15b --- /dev/null +++ b/pkg/liqoctl/rest/identity/generate.go @@ -0,0 +1,174 @@ +// 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 identity + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/runtime" + + discoveryv1alpha1 "github.com/liqotech/liqo/apis/discovery/v1alpha1" + "github.com/liqotech/liqo/pkg/auth" + identitymanager "github.com/liqotech/liqo/pkg/identityManager" + "github.com/liqotech/liqo/pkg/liqoctl/completion" + "github.com/liqotech/liqo/pkg/liqoctl/output" + "github.com/liqotech/liqo/pkg/liqoctl/rest" + peeringroles "github.com/liqotech/liqo/pkg/peering-roles" + tenantnamespace "github.com/liqotech/liqo/pkg/tenantNamespace" + "github.com/liqotech/liqo/pkg/utils" + "github.com/liqotech/liqo/pkg/utils/apiserver" + "github.com/liqotech/liqo/pkg/utils/args" + csrutil "github.com/liqotech/liqo/pkg/utils/csr" +) + +const liqoctlGenerateIdentityHelp = `Generate a local identity to be used by a remote cluster.` + +// Generate generates an Identity. +func (o *Options) Generate(ctx context.Context, options *rest.GenerateOptions) *cobra.Command { + outputFormat := args.NewEnum([]string{"command", "json", "yaml"}, "command") + + o.generateOptions = options + + cmd := &cobra.Command{ + Use: "identity", + Aliases: []string{"identities"}, + Short: "Generate an Identity", + Long: liqoctlGenerateIdentityHelp, + Args: cobra.NoArgs, + + PreRun: func(cmd *cobra.Command, args []string) { + options.OutputFormat = outputFormat.Value + o.generateOptions = options + }, + + Run: func(cmd *cobra.Command, args []string) { + err := o.handleGenerate(ctx) + if err != nil { + o.generateOptions.Printer.ExitWithMessage(output.PrettyErr(err)) + } + }, + } + + cmd.Flags().VarP(outputFormat, "output", "o", + "Output format of the resulting Identity resource. Supported formats: command, json, yaml") + cmd.Flags().StringVar(&o.remoteClusterID, "remote-cluster-id", "", + "Cluster ID of the remote cluster.") + cmd.Flags().StringVar(&o.remoteClusterName, "remote-cluster-name", "", + "Cluster name of the remote cluster.") + + runtime.Must(cmd.RegisterFlagCompletionFunc("output", completion.Enumeration(outputFormat.Allowed))) + + runtime.Must(cmd.MarkFlagRequired("remote-cluster-id")) + runtime.Must(cmd.MarkFlagRequired("remote-cluster-name")) + + return cmd +} + +func (o *Options) handleGenerate(ctx context.Context) error { + opts := o.generateOptions + + // generate request + + localClusterIdentity, err := utils.GetClusterIdentityWithControllerClient(ctx, opts.CRClient, opts.LiqoNamespace) + if err != nil { + return err + } + + remoteClusterIdentity := discoveryv1alpha1.ClusterIdentity{ + ClusterID: o.remoteClusterID, + ClusterName: o.remoteClusterName, + } + + key, csr, err := csrutil.NewKeyAndRequest(remoteClusterIdentity.ClusterID) + if err != nil { + return fmt.Errorf("failed to create create identity: %w", err) + } + + identityRequest := auth.NewCertificateIdentityRequest(remoteClusterIdentity, "", "", csr) + + // handle request + + // TODO + var awsConfig identitymanager.AwsConfig + + namespaceManager := tenantnamespace.NewCachedManager(ctx, opts.KubeClient) + var identityProvider identitymanager.IdentityProvider + if awsConfig.IsEmpty() { + identityProvider = identitymanager.NewCertificateIdentityProvider( + ctx, opts.KubeClient, remoteClusterIdentity, namespaceManager) + } else { + identityProvider = identitymanager.NewIAMIdentityProvider( + opts.KubeClient, remoteClusterIdentity, &awsConfig, namespaceManager) + } + idManager := identitymanager.NewCertificateIdentityManager(opts.KubeClient, remoteClusterIdentity, namespaceManager) + + namespace, err := namespaceManager.CreateNamespace(ctx, remoteClusterIdentity) + if err != nil { + return err + } + + // issue certificate request + identityResponse, err := identityProvider.ApproveSigningRequest( + remoteClusterIdentity, identityRequest.CertificateSigningRequest) + if err != nil { + return err + } + + peeringPermission, err := peeringroles.GetPeeringPermission(ctx, opts.KubeClient) + if err != nil { + return err + } + + // bind basic permission required to start the peering + if _, err = namespaceManager.BindClusterRoles( + ctx, remoteClusterIdentity, peeringPermission.Basic...); err != nil { + return err + } + + var apiServerConfig apiserver.Config + // make the response to send to the remote cluster + response, err := auth.NewCertificateIdentityResponse(namespace.Name, identityResponse, apiServerConfig) + if err != nil { + return err + } + + // store the identity in a secret + secret, err := idManager.GenerateIdentitySecret(ctx, remoteClusterIdentity, "", key, opts.Namespace, response) + if err != nil { + return err + } + + switch opts.OutputFormat { + case "json", "yaml": + opts.Printer.CheckErr(o.output(secret)) + case "command": + certificate := base64.StdEncoding.EncodeToString(secret.Data["certificate"]) + privateKey := base64.StdEncoding.EncodeToString(secret.Data["private-key"]) + + command := strings.Join([]string{ + o.generateOptions.Liqoctl, "update identity", + "--remote-cluster-id", localClusterIdentity.ClusterID, + "--remote-cluster-name", localClusterIdentity.ClusterName, + "--certificate", certificate, + "--private-key", privateKey, + }, " ") + fmt.Println(command) + } + return nil +} diff --git a/pkg/liqoctl/rest/identity/get.go b/pkg/liqoctl/rest/identity/get.go new file mode 100644 index 0000000000..5e181d77c2 --- /dev/null +++ b/pkg/liqoctl/rest/identity/get.go @@ -0,0 +1,28 @@ +// 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 identity + +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/identity/types.go b/pkg/liqoctl/rest/identity/types.go new file mode 100644 index 0000000000..791305d784 --- /dev/null +++ b/pkg/liqoctl/rest/identity/types.go @@ -0,0 +1,48 @@ +// 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 identity + +import ( + discoveryv1alpha1 "github.com/liqotech/liqo/apis/discovery/v1alpha1" + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// Options encapsulates the arguments of the identity command. +type Options struct { + generateOptions *rest.GenerateOptions + updateOptions *rest.UpdateOptions + + RemoteClusterIdentity discoveryv1alpha1.ClusterIdentity + CertificateString string + PrivateKeyString string + + remoteClusterID string + remoteClusterName string +} + +var _ rest.API = &Options{} + +// Identity returns the rest API for the identity command. +func Identity() rest.API { + return &Options{} +} + +// APIOptions returns the APIOptions for the identity API. +func (o *Options) APIOptions() *rest.APIOptions { + return &rest.APIOptions{ + EnableGenerate: true, + EnableUpdate: true, + } +} diff --git a/pkg/liqoctl/rest/identity/update.go b/pkg/liqoctl/rest/identity/update.go new file mode 100644 index 0000000000..b494dfb6b3 --- /dev/null +++ b/pkg/liqoctl/rest/identity/update.go @@ -0,0 +1,156 @@ +// 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 identity + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/spf13/cobra" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + identitymanager "github.com/liqotech/liqo/pkg/identityManager" + "github.com/liqotech/liqo/pkg/liqoctl/output" + "github.com/liqotech/liqo/pkg/liqoctl/rest" + tenantnamespace "github.com/liqotech/liqo/pkg/tenantNamespace" + "github.com/liqotech/liqo/pkg/utils" +) + +const liqoctlUpdateIdentityHelp = `Generate a local identity to be used by a remote cluster.` + +// Update implements the update command. +func (o *Options) Update(ctx context.Context, options *rest.UpdateOptions) *cobra.Command { + o.updateOptions = options + + cmd := &cobra.Command{ + Use: "identity", + Aliases: []string{"identities"}, + Short: "Update an Identity", + Long: liqoctlUpdateIdentityHelp, + Args: cobra.NoArgs, + + PreRun: func(cmd *cobra.Command, args []string) { + o.updateOptions = options + }, + + Run: func(cmd *cobra.Command, args []string) { + err := o.handleUpdate(ctx) + if err != nil { + o.updateOptions.Printer.ExitWithMessage(output.PrettyErr(err)) + } + }, + } + + cmd.Flags().StringVar(&o.RemoteClusterIdentity.ClusterID, "remote-cluster-id", "", "The cluster ID of the remote cluster") + cmd.Flags().StringVar(&o.RemoteClusterIdentity.ClusterName, "remote-cluster-name", "", "The cluster name of the remote cluster") + cmd.Flags().StringVar(&o.CertificateString, "certificate", "", "The certificate of the remote cluster") + cmd.Flags().StringVar(&o.PrivateKeyString, "private-key", "", "The private key for the remote cluster") + + runtime.Must(cmd.MarkFlagRequired("remote-cluster-id")) + runtime.Must(cmd.MarkFlagRequired("remote-cluster-name")) + runtime.Must(cmd.MarkFlagRequired("certificate")) + runtime.Must(cmd.MarkFlagRequired("private-key")) + + return cmd +} + +func (o *Options) handleUpdate(ctx context.Context) error { + opts := o.updateOptions + + localClusterIdentity, err := utils.GetClusterIdentityWithControllerClient(ctx, opts.CRClient, opts.LiqoNamespace) + if err != nil { + return err + } + + namespaceManager := tenantnamespace.NewCachedManager(ctx, opts.KubeClient) + idManager := identitymanager.NewCertificateIdentityManager(opts.KubeClient, localClusterIdentity, namespaceManager) + + secret, err := idManager.GetSecret(o.RemoteClusterIdentity) + if err != nil { + return err + } + + certificate, err := base64.StdEncoding.DecodeString(o.CertificateString) + if err != nil { + return fmt.Errorf("failed to decode certificate: %w", err) + } + secret.Data["certificate"] = certificate + + privateKey, err := base64.StdEncoding.DecodeString(o.PrivateKeyString) + if err != nil { + return fmt.Errorf("failed to decode private key: %w", err) + } + secret.Data["private-key"] = privateKey + + _, err = opts.KubeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + return err + } + + deployments, err := getDeploymentsToBeRestarted(ctx, opts.CRClient, opts.LiqoNamespace, secret.Namespace) + if err != nil { + return err + } + + for _, deployment := range deployments { + if err := utils.RestartDeployment(ctx, opts.CRClient, deployment); err != nil { + return err + } + } + + return nil +} + +func getDeploymentsToBeRestarted(ctx context.Context, cl client.Client, + liqoNamespace, tenantNamespace string) ([]*appsv1.Deployment, error) { + var deployments appsv1.DeploymentList + var deploymentsToBeRestarted []*appsv1.Deployment + + // controller manager + if err := cl.List(ctx, &deployments, client.InNamespace(liqoNamespace), client.MatchingLabels{ + "app.kubernetes.io/name": "controller-manager", + }); err != nil { + return nil, err + } + for i := range deployments.Items { + deploymentsToBeRestarted = append(deploymentsToBeRestarted, &deployments.Items[i]) + } + + // crd replicator + if err := cl.List(ctx, &deployments, client.InNamespace(liqoNamespace), client.MatchingLabels{ + "app.kubernetes.io/name": "crd-replicator", + }); err != nil { + return nil, err + } + for i := range deployments.Items { + deploymentsToBeRestarted = append(deploymentsToBeRestarted, &deployments.Items[i]) + } + + // virtual kubelet + if err := cl.List(ctx, &deployments, client.InNamespace(tenantNamespace), client.MatchingLabels{ + "app.kubernetes.io/name": "virtual-kubelet", + }); err != nil { + return nil, err + } + for i := range deployments.Items { + deploymentsToBeRestarted = append(deploymentsToBeRestarted, &deployments.Items[i]) + } + + return deploymentsToBeRestarted, nil +} diff --git a/pkg/liqoctl/rest/types.go b/pkg/liqoctl/rest/types.go index 4a8c4ab2e2..fa2e9e795e 100644 --- a/pkg/liqoctl/rest/types.go +++ b/pkg/liqoctl/rest/types.go @@ -64,6 +64,7 @@ type GenerateOptions struct { *factory.Factory OutputFormat string + Liqoctl string } // API is the interface that must be implemented by the API. diff --git a/pkg/liqoctl/update/doc.go b/pkg/liqoctl/update/doc.go new file mode 100644 index 0000000000..50975aca9e --- /dev/null +++ b/pkg/liqoctl/update/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 update contains the implementation of the 'update' command +package update diff --git a/pkg/liqoctl/update/types.go b/pkg/liqoctl/update/types.go new file mode 100644 index 0000000000..cded47e3ff --- /dev/null +++ b/pkg/liqoctl/update/types.go @@ -0,0 +1,52 @@ +// 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 update + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/liqotech/liqo/pkg/liqoctl/factory" + "github.com/liqotech/liqo/pkg/liqoctl/rest" +) + +// NewUpdateCommand returns the cobra command for the update subcommand. +func NewUpdateCommand(ctx context.Context, liqoResources []rest.APIProvider, f *factory.Factory) *cobra.Command { + options := &rest.UpdateOptions{ + Factory: f, + } + + cmd := &cobra.Command{ + Use: "update", + Short: "Update Liqo resources", + Long: "Update Liqo resources.", + Args: cobra.NoArgs, + } + + f.AddNamespaceFlag(cmd.PersistentFlags()) + f.AddLiqoNamespaceFlag(cmd.PersistentFlags()) + + for _, r := range liqoResources { + api := r() + + apiOptions := api.APIOptions() + if apiOptions.EnableUpdate { + cmd.AddCommand(api.Update(ctx, options)) + } + } + + return cmd +} diff --git a/pkg/utils/deployment.go b/pkg/utils/deployment.go new file mode 100644 index 0000000000..e4f5a203da --- /dev/null +++ b/pkg/utils/deployment.go @@ -0,0 +1,81 @@ +package utils + +import ( + "context" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// RestartDeployment restarts the provided deployment by evicting all its pods. +func RestartDeployment(ctx context.Context, cl client.Client, deploy *appsv1.Deployment) error { + var podList corev1.PodList + if err := cl.List(ctx, &podList, client.InNamespace(deploy.Namespace), + client.MatchingLabels(deploy.Spec.Selector.MatchLabels)); err != nil { + return err + } + + return EvictPods(ctx, cl, &podList, 3*time.Second) +} + +// EvictPods performs the eviction of the provided list of pods in parallel, waiting for their deletion. +func EvictPods(ctx context.Context, cl client.Client, podList *corev1.PodList, checkPeriod time.Duration) error { + for i := range podList.Items { + if err := EvictPod(ctx, cl, &podList.Items[i]); err != nil { + return err + } + } + + for i := range podList.Items { + if err := WaitPodForDelete(ctx, cl, &podList.Items[i], checkPeriod); err != nil { + return err + } + } + + return nil +} + +// EvictPod evicts the provided pod and waits for its deletion. +func EvictPod(ctx context.Context, cl client.Client, pod *corev1.Pod) error { + eviction := &policyv1.Eviction{ + ObjectMeta: metav1.ObjectMeta{ + Name: pod.Name, + Namespace: pod.Namespace, + }, + DeleteOptions: &metav1.DeleteOptions{}, + } + + if err := cl.SubResource("eviction").Create(ctx, pod, eviction); err != nil { + return err + } + + klog.V(4).Infof("Drain node %s -> pod %v/%v eviction started", pod.Spec.NodeName, pod.Namespace, pod.Name) + + return nil +} + +// waitForDelete waits for the pod deletion. +func WaitPodForDelete(ctx context.Context, cl client.Client, pod *corev1.Pod, checkPeriod time.Duration) error { + return wait.PollUntilContextCancel(ctx, checkPeriod, true, func(ctx context.Context) (bool, error) { + klog.Infof("Drain node %s -> pod %v/%v waiting for deletion", pod.Spec.NodeName, pod.Namespace, pod.Name) + updatedPod := &corev1.Pod{} + err := cl.Get(ctx, client.ObjectKey{Namespace: pod.Namespace, Name: pod.Name}, updatedPod) + if apierrors.IsNotFound(err) || (updatedPod != nil && + pod.ObjectMeta.UID != updatedPod.ObjectMeta.UID) { + klog.Infof("Drain node %s -> pod %v/%v successfully deleted", pod.Spec.NodeName, pod.Namespace, pod.Name) + return true, nil + } + if err != nil { + klog.Error(err) + return false, err + } + return false, nil + }) +}