github.com/pluralsh/plural-cli@v0.9.5/cmd/plural/bootstrap.go (about)

     1  package plural
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/pluralsh/plural-cli/pkg/kubernetes"
    12  	"github.com/pluralsh/plural-cli/pkg/manifest"
    13  	"github.com/pluralsh/plural-cli/pkg/provider"
    14  	"github.com/pluralsh/plural-cli/pkg/utils"
    15  	"github.com/urfave/cli"
    16  	corev1 "k8s.io/api/core/v1"
    17  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    18  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/runtime"
    21  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    22  	"k8s.io/client-go/rest"
    23  	"k8s.io/client-go/tools/clientcmd"
    24  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    25  	clusterapioperator "sigs.k8s.io/cluster-api-operator/api/v1alpha1"
    26  	clusterapi "sigs.k8s.io/cluster-api/api/v1beta1"
    27  	apiclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client"
    28  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    29  )
    30  
    31  var runtimescheme = runtime.NewScheme()
    32  
    33  func init() {
    34  	utilruntime.Must(corev1.AddToScheme(runtimescheme))
    35  	utilruntime.Must(apiextensionsv1.AddToScheme(runtimescheme))
    36  	utilruntime.Must(clusterapi.AddToScheme(runtimescheme))
    37  	utilruntime.Must(clusterapioperator.AddToScheme(runtimescheme))
    38  }
    39  
    40  const (
    41  	kindConfig = `kind: Cluster
    42  apiVersion: kind.x-k8s.io/v1alpha4
    43  networking:
    44    ipFamily: dual
    45  nodes:
    46  - role: control-plane
    47    extraMounts:
    48      - hostPath: /var/run/docker.sock
    49        containerPath: /var/run/docker.sock`
    50  )
    51  
    52  func (p *Plural) bootstrapCommands() []cli.Command {
    53  	return []cli.Command{
    54  		{
    55  			Name:        "cluster",
    56  			Subcommands: p.bootstrapClusterCommands(),
    57  			Usage:       "Manage bootstrap cluster",
    58  		},
    59  		{
    60  			Name:        "namespace",
    61  			Subcommands: p.namespaceCommands(),
    62  			Usage:       "Manage bootstrap cluster",
    63  		},
    64  	}
    65  }
    66  
    67  func (p *Plural) namespaceCommands() []cli.Command {
    68  	return []cli.Command{
    69  		{
    70  			Name:      "create",
    71  			ArgsUsage: "NAME",
    72  			Usage:     "Creates bootstrap namespace",
    73  			Flags: []cli.Flag{
    74  				cli.BoolFlag{
    75  					Name:  "skip-if-exists",
    76  					Usage: "skip creating when namespace exists",
    77  				},
    78  			},
    79  			Action: latestVersion(initKubeconfig(requireArgs(p.handleCreateNamespace, []string{"NAME"}))),
    80  		},
    81  	}
    82  }
    83  
    84  func (p *Plural) bootstrapClusterCommands() []cli.Command {
    85  	return []cli.Command{
    86  		{
    87  			Name:      "create",
    88  			ArgsUsage: "NAME",
    89  			Usage:     "Creates bootstrap cluster",
    90  			Flags: []cli.Flag{
    91  				cli.StringFlag{
    92  					Name:  "image",
    93  					Usage: "kind image to use",
    94  				},
    95  				cli.BoolFlag{
    96  					Name:  "skip-if-exists",
    97  					Usage: "skip creating when cluster exists",
    98  				},
    99  			},
   100  			Action: latestVersion(requireKind(requireArgs(handleCreateCluster, []string{"NAME"}))),
   101  		},
   102  		{
   103  			Name:      "delete",
   104  			ArgsUsage: "NAME",
   105  			Usage:     "Deletes bootstrap cluster",
   106  			Action:    latestVersion(requireKind(requireArgs(handleDeleteCluster, []string{"NAME"}))),
   107  		},
   108  		{
   109  			Name:  "move",
   110  			Usage: "Move cluster API objects",
   111  			Flags: []cli.Flag{
   112  				cli.StringFlag{
   113  					Name:  "kubeconfig",
   114  					Usage: "path to the kubeconfig file for the source management cluster. If unspecified, default discovery rules apply.",
   115  				},
   116  				cli.StringFlag{
   117  					Name:  "kubeconfig-context",
   118  					Usage: "context to be used within the kubeconfig file for the source management cluster. If empty, current context will be used.",
   119  				},
   120  				cli.StringFlag{
   121  					Name:  "to-kubeconfig",
   122  					Usage: "path to the kubeconfig file to use for the destination management cluster.",
   123  				},
   124  				cli.StringFlag{
   125  					Name:  "to-kubeconfig-context",
   126  					Usage: "Context to be used within the kubeconfig file for the destination management cluster. If empty, current context will be used.",
   127  				},
   128  			},
   129  			Action: latestVersion(p.handleMoveCluster),
   130  		},
   131  		{
   132  			Name:      "destroy-cluster-api",
   133  			ArgsUsage: "NAME",
   134  			Usage:     "Destroy cluster API",
   135  			Action:    latestVersion(requireArgs(p.handleDestroyClusterAPI, []string{"NAME"})),
   136  		},
   137  	}
   138  }
   139  
   140  func (p *Plural) handleDestroyClusterAPI(c *cli.Context) error {
   141  	name := c.Args().Get(0)
   142  	_, found := utils.ProjectRoot()
   143  	if !found {
   144  		return fmt.Errorf("You're not within an installation repo")
   145  	}
   146  	pm, err := manifest.FetchProject()
   147  	if err != nil {
   148  		return err
   149  	}
   150  	prov := &provider.KINDProvider{Clust: "bootstrap"}
   151  	if err := prov.KubeConfig(); err != nil {
   152  		return err
   153  	}
   154  	config, err := kubernetes.KubeConfig()
   155  	if err != nil {
   156  		return err
   157  	}
   158  	client, err := genClientFromConfig(config)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	utils.Warn("Waiting for the operator ")
   163  	if err := utils.WaitFor(20*time.Minute, 10*time.Second, func() (bool, error) {
   164  		pods := &corev1.PodList{}
   165  		providerName := pm.Provider
   166  		if providerName == "kind" {
   167  			providerName = "docker"
   168  		}
   169  		selector := fmt.Sprintf("infrastructure-%s", strings.ToLower(providerName))
   170  		if err := client.List(context.Background(), pods, ctrlruntimeclient.MatchingLabels{"cluster.x-k8s.io/provider": selector}); err != nil {
   171  			if !apierrors.IsNotFound(err) {
   172  				return false, fmt.Errorf("failed to get pods: %w", err)
   173  			}
   174  			return false, nil
   175  		}
   176  		if len(pods.Items) > 0 {
   177  			if isReady(pods.Items[0].Status.Conditions) {
   178  				return true, nil
   179  			}
   180  		}
   181  		utils.Warn(".")
   182  		return false, nil
   183  	}); err != nil {
   184  		return err
   185  	}
   186  	if err := client.Delete(context.Background(), &clusterapi.Cluster{
   187  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "bootstrap"},
   188  	}); err != nil {
   189  		return err
   190  	}
   191  	utils.Warn("\nDeleting cluster")
   192  	return utils.WaitFor(40*time.Minute, 10*time.Second, func() (bool, error) {
   193  		if err := client.Get(context.Background(), ctrlruntimeclient.ObjectKey{Name: name, Namespace: "bootstrap"}, &clusterapi.Cluster{}); err != nil {
   194  			if !apierrors.IsNotFound(err) {
   195  				return false, fmt.Errorf("failed to get Cluster: %w", err)
   196  			}
   197  			return true, nil
   198  		}
   199  		utils.Warn(".")
   200  		return false, nil
   201  	})
   202  }
   203  
   204  func isReady(conditions []corev1.PodCondition) bool {
   205  	for _, cond := range conditions {
   206  		if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
   207  			return true
   208  		}
   209  	}
   210  	return false
   211  }
   212  
   213  func (p *Plural) handleMoveCluster(c *cli.Context) error {
   214  	_, found := utils.ProjectRoot()
   215  	if !found {
   216  		return fmt.Errorf("You're not within an installation repo")
   217  	}
   218  
   219  	client, err := apiclient.New("")
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	kubeconfig := c.String("kubeconfig")
   225  	kubeconfigContext := c.String("kubeconfig-context")
   226  	toKubeconfig := c.String("to-kubeconfig")
   227  	toKubeconfigContext := c.String("to-kubeconfig-context")
   228  
   229  	options := apiclient.MoveOptions{
   230  		FromKubeconfig: apiclient.Kubeconfig{
   231  			Path:    kubeconfig,
   232  			Context: kubeconfigContext,
   233  		},
   234  		ToKubeconfig: apiclient.Kubeconfig{
   235  			Path:    toKubeconfig,
   236  			Context: toKubeconfigContext,
   237  		},
   238  		Namespace: "bootstrap",
   239  		DryRun:    false,
   240  	}
   241  	if err := client.Move(options); err != nil {
   242  		return err
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  func (p *Plural) handleCreateNamespace(c *cli.Context) error {
   249  	name := c.Args().Get(0)
   250  	fmt.Printf("Creating namespace %s ...\n", name)
   251  	err := p.InitKube()
   252  	if err != nil {
   253  		return err
   254  	}
   255  	if err := p.CreateNamespace(name, true); err != nil {
   256  		if apierrors.IsAlreadyExists(err) {
   257  			return nil
   258  		}
   259  		return err
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func handleDeleteCluster(c *cli.Context) error {
   266  	name := c.Args().Get(0)
   267  	return utils.Exec("kind", "delete", "cluster", "--name", name)
   268  }
   269  
   270  func handleCreateCluster(c *cli.Context) error {
   271  	name := c.Args().Get(0)
   272  	imageFlag := c.String("image")
   273  	skipCreation := c.Bool("skip-if-exists")
   274  	if utils.IsKindClusterAlreadyExists(name) && skipCreation {
   275  		utils.Highlight("Cluster %s already exists \n", name)
   276  		return nil
   277  	}
   278  
   279  	dir, err := os.MkdirTemp("", "kind")
   280  	if err != nil {
   281  		return err
   282  	}
   283  	defer os.RemoveAll(dir)
   284  	config := path.Join(dir, "config.yaml")
   285  	if err := os.WriteFile(config, []byte(kindConfig), 0644); err != nil {
   286  		return err
   287  	}
   288  	args := []string{"create", "cluster", "--name", name, "--config", config}
   289  	if imageFlag != "" {
   290  		args = append(args, "--image", imageFlag)
   291  	}
   292  
   293  	if err := utils.Exec("kind", args...); err != nil {
   294  		return err
   295  	}
   296  
   297  	kubeconfig, err := utils.GetKindClusterKubeconfig(name, false)
   298  	if err != nil {
   299  		return err
   300  	}
   301  
   302  	client, err := getClient(kubeconfig)
   303  	if err != nil {
   304  		return err
   305  	}
   306  	if err := client.Create(context.Background(), &corev1.Namespace{
   307  		ObjectMeta: metav1.ObjectMeta{
   308  			Name: "bootstrap",
   309  		},
   310  	}); err != nil {
   311  		return err
   312  	}
   313  	internalKubeconfig, err := utils.GetKindClusterKubeconfig(name, true)
   314  	if err != nil {
   315  		return err
   316  	}
   317  	kubeconfigSecret := &corev1.Secret{
   318  		ObjectMeta: metav1.ObjectMeta{
   319  			Name:      "kubeconfig",
   320  			Namespace: "bootstrap",
   321  		},
   322  		Data: map[string][]byte{
   323  			"value": []byte(internalKubeconfig),
   324  		},
   325  	}
   326  	if err := client.Create(context.Background(), kubeconfigSecret); err != nil {
   327  		return err
   328  	}
   329  
   330  	return nil
   331  }
   332  
   333  func getClient(rawKubeconfig string) (ctrlruntimeclient.Client, error) {
   334  
   335  	cfg, err := clientcmd.Load([]byte(rawKubeconfig))
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	clientConfig, err := getRestConfig(cfg)
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  
   344  	return genClientFromConfig(clientConfig)
   345  }
   346  
   347  func genClientFromConfig(cfg *rest.Config) (ctrlruntimeclient.Client, error) {
   348  	return ctrlruntimeclient.New(cfg, ctrlruntimeclient.Options{
   349  		Scheme: runtimescheme,
   350  	})
   351  }
   352  
   353  func getRestConfig(cfg *clientcmdapi.Config) (*rest.Config, error) {
   354  	iconfig := clientcmd.NewNonInteractiveClientConfig(
   355  		*cfg,
   356  		"",
   357  		&clientcmd.ConfigOverrides{},
   358  		nil,
   359  	)
   360  
   361  	clientConfig, err := iconfig.ClientConfig()
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	// Avoid blocking of the controller by increasing the QPS for user cluster interaction
   367  	clientConfig.QPS = 20
   368  	clientConfig.Burst = 50
   369  
   370  	return clientConfig, nil
   371  }