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 }