github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/create_subcmds.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package cluster
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  
    27  	"github.com/spf13/cobra"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    32  
    33  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    34  	"github.com/1aal/kubeblocks/pkg/cli/create"
    35  	"github.com/1aal/kubeblocks/pkg/cli/edit"
    36  	"github.com/1aal/kubeblocks/pkg/cli/types"
    37  	"github.com/1aal/kubeblocks/pkg/cli/util"
    38  )
    39  
    40  type objectInfo struct {
    41  	gvr schema.GroupVersionResource
    42  	obj *unstructured.Unstructured
    43  }
    44  
    45  type createSubCmdsOptions struct {
    46  	// clusterType is the type of the cluster to create.
    47  	clusterType cluster.ClusterType
    48  
    49  	// values is used to render the cluster helm chartInfo.
    50  	values map[string]interface{}
    51  
    52  	// chartInfo is the cluster chart information, used to render the command flag
    53  	// and validate the values.
    54  	chartInfo *cluster.ChartInfo
    55  
    56  	*create.CreateOptions
    57  }
    58  
    59  func newSubCmdsOptions(createOptions *create.CreateOptions, t cluster.ClusterType) (*createSubCmdsOptions, error) {
    60  	var err error
    61  	o := &createSubCmdsOptions{
    62  		CreateOptions: createOptions,
    63  		clusterType:   t,
    64  	}
    65  
    66  	if o.chartInfo, err = cluster.BuildChartInfo(t); err != nil {
    67  		return nil, err
    68  	}
    69  	return o, nil
    70  }
    71  
    72  func buildCreateSubCmds(createOptions *create.CreateOptions) []*cobra.Command {
    73  	var cmds []*cobra.Command
    74  
    75  	for _, t := range cluster.SupportedTypes() {
    76  		o, err := newSubCmdsOptions(createOptions, t)
    77  		if err != nil {
    78  			fmt.Fprintf(os.Stdout, "Failed add '%s' to 'create' sub command due to %s\n", t.String(), err.Error())
    79  			cluster.ClearCharts(t)
    80  			continue
    81  		}
    82  
    83  		cmd := &cobra.Command{
    84  			Use:     t.String() + " NAME",
    85  			Short:   fmt.Sprintf("Create a %s cluster.", t),
    86  			Example: buildCreateSubCmdsExamples(t),
    87  			Run: func(cmd *cobra.Command, args []string) {
    88  				o.Args = args
    89  				cmdutil.CheckErr(o.CreateOptions.Complete())
    90  				cmdutil.CheckErr(o.complete(cmd))
    91  				cmdutil.CheckErr(o.validate())
    92  				cmdutil.CheckErr(o.run())
    93  			},
    94  		}
    95  
    96  		if o.chartInfo.Alias != "" {
    97  			cmd.Aliases = []string{o.chartInfo.Alias}
    98  		}
    99  
   100  		util.CheckErr(addCreateFlags(cmd, o.Factory, o.chartInfo))
   101  		cmds = append(cmds, cmd)
   102  	}
   103  	return cmds
   104  }
   105  
   106  func (o *createSubCmdsOptions) complete(cmd *cobra.Command) error {
   107  	var err error
   108  
   109  	// if name is not specified, generate a random cluster name
   110  	if o.Name == "" {
   111  		o.Name, err = generateClusterName(o.Dynamic, o.Namespace)
   112  		if err != nil {
   113  			return err
   114  		}
   115  	}
   116  
   117  	// get values from flags
   118  	o.values = getValuesFromFlags(cmd.LocalNonPersistentFlags())
   119  
   120  	// get all the rendered objects
   121  	objs, err := o.getObjectsInfo()
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	// find the cluster object
   127  	clusterObj, err := o.getClusterObj(objs)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// get clusterDef name
   133  	spec, ok := clusterObj.Object["spec"].(map[string]interface{})
   134  	if !ok {
   135  		return fmt.Errorf("cannot find spec in cluster object")
   136  	}
   137  	clusterDef, ok := spec["clusterDefinitionRef"].(string)
   138  	if !ok {
   139  		return fmt.Errorf("cannot find clusterDefinitionRef in cluster spec")
   140  	}
   141  	o.chartInfo.ClusterDef = clusterDef
   142  
   143  	return nil
   144  }
   145  
   146  func (o *createSubCmdsOptions) validate() error {
   147  	if err := o.validateVersion(); err != nil {
   148  		return err
   149  	}
   150  	return cluster.ValidateValues(o.chartInfo, o.values)
   151  }
   152  
   153  func (o *createSubCmdsOptions) run() error {
   154  
   155  	objs, err := o.getObjectsInfo()
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	getClusterObj := func() (*unstructured.Unstructured, error) {
   161  		for _, obj := range objs {
   162  			if obj.gvr == types.ClusterGVR() {
   163  				return obj.obj, nil
   164  			}
   165  		}
   166  		return nil, fmt.Errorf("failed to find cluster object from manifests rendered from %s chart", o.clusterType)
   167  	}
   168  
   169  	// only edits the cluster object, other dependency objects are created directly
   170  	if o.EditBeforeCreate {
   171  		clusterObj, err := getClusterObj()
   172  		if err != nil {
   173  			return err
   174  		}
   175  		customEdit := edit.NewCustomEditOptions(o.Factory, o.IOStreams, "create")
   176  		if err = customEdit.Run(clusterObj); err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	dryRun, err := o.GetDryRunStrategy()
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// create cluster and dependency resources
   187  	for _, obj := range objs {
   188  		isCluster := obj.gvr == types.ClusterGVR()
   189  		resObj := obj.obj
   190  
   191  		if dryRun != create.DryRunClient {
   192  			createOptions := metav1.CreateOptions{}
   193  			if dryRun == create.DryRunServer {
   194  				createOptions.DryRun = []string{metav1.DryRunAll}
   195  			}
   196  
   197  			// create resource
   198  			resObj, err = o.Dynamic.Resource(obj.gvr).Namespace(o.Namespace).Create(context.TODO(), resObj, createOptions)
   199  			if err != nil {
   200  				return err
   201  			}
   202  
   203  			// only output cluster resource
   204  			if dryRun != create.DryRunServer && isCluster {
   205  				if o.Quiet {
   206  					continue
   207  				}
   208  				if o.CustomOutPut != nil {
   209  					o.CustomOutPut(o.CreateOptions)
   210  				}
   211  				fmt.Fprintf(o.Out, "%s %s created\n", resObj.GetKind(), resObj.GetName())
   212  				continue
   213  			}
   214  		}
   215  
   216  		if len(objs) > 1 {
   217  			fmt.Fprintf(o.Out, "---\n")
   218  		}
   219  
   220  		p, err := o.ToPrinter(nil, false)
   221  		if err != nil {
   222  			return err
   223  		}
   224  
   225  		if err = p.PrintObj(resObj, o.Out); err != nil {
   226  			return err
   227  		}
   228  	}
   229  	return nil
   230  }
   231  
   232  func (o *createSubCmdsOptions) validateVersion() error {
   233  	var err error
   234  	cv, ok := o.values[cluster.VersionSchemaProp.String()].(string)
   235  	if ok && cv != "" {
   236  		if err = cluster.ValidateClusterVersion(o.Dynamic, o.chartInfo.ClusterDef, cv); err != nil {
   237  			return fmt.Errorf("cluster version \"%s\" does not exist, run following command to get the available cluster versions\n\tkbcli cv list --cluster-definition=%s",
   238  				cv, o.chartInfo.ClusterDef)
   239  		}
   240  		return nil
   241  	}
   242  
   243  	cv, err = cluster.GetDefaultVersion(o.Dynamic, o.chartInfo.ClusterDef)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	// set cluster version
   248  	o.values[cluster.VersionSchemaProp.String()] = cv
   249  
   250  	dryRun, err := o.GetDryRunStrategy()
   251  	if err != nil {
   252  		return err
   253  	}
   254  	// if dryRun is set, run in quiet mode, avoid to output yaml file with the info
   255  	if dryRun != create.DryRunNone {
   256  		return nil
   257  	}
   258  
   259  	fmt.Fprintf(o.Out, "Info: --version is not specified, %s is applied by default.\n", cv)
   260  	return nil
   261  }
   262  
   263  func (o *createSubCmdsOptions) getObjectsInfo() ([]*objectInfo, error) {
   264  	// move values that belong to sub chart to sub map
   265  	values := buildHelmValues(o.chartInfo, o.values)
   266  
   267  	// get Kubernetes version
   268  	kubeVersion, err := util.GetK8sVersion(o.Client.Discovery())
   269  	if err != nil || kubeVersion == "" {
   270  		return nil, fmt.Errorf("failed to get Kubernetes version %v", err)
   271  	}
   272  
   273  	// get cluster manifests
   274  	manifests, err := cluster.GetManifests(o.chartInfo.Chart, o.Namespace, o.Name, kubeVersion, values)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  
   279  	// get objects to be created from manifests
   280  	return getObjectsInfo(o.Factory, manifests)
   281  }
   282  
   283  func (o *createSubCmdsOptions) getClusterObj(objs []*objectInfo) (*unstructured.Unstructured, error) {
   284  	for _, obj := range objs {
   285  		if obj.gvr == types.ClusterGVR() {
   286  			return obj.obj, nil
   287  		}
   288  	}
   289  	return nil, fmt.Errorf("failed to find cluster object from manifests rendered from %s chart", o.clusterType)
   290  }