github.com/oam-dev/kubevela@v1.9.11/references/cli/init.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"strconv"
    25  	"time"
    26  
    27  	"cuelang.org/go/cue"
    28  	"github.com/AlecAivazis/survey/v2"
    29  	"github.com/fatih/color"
    30  	"github.com/spf13/cobra"
    31  	"github.com/spf13/pflag"
    32  	v1 "k8s.io/api/core/v1"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"github.com/oam-dev/kubevela/apis/types"
    38  	common2 "github.com/oam-dev/kubevela/pkg/utils/common"
    39  	cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
    40  	"github.com/oam-dev/kubevela/references/appfile"
    41  	"github.com/oam-dev/kubevela/references/appfile/api"
    42  	"github.com/oam-dev/kubevela/references/common"
    43  	"github.com/oam-dev/kubevela/references/docgen"
    44  )
    45  
    46  type appInitOptions struct {
    47  	client client.Client
    48  	cmdutil.IOStreams
    49  	Namespace string
    50  	c         common2.Args
    51  
    52  	app          *api.Application
    53  	appName      string
    54  	workloadName string
    55  	workloadType string
    56  	renderOnly   bool
    57  }
    58  
    59  // NewInitCommand creates `init` command
    60  func NewInitCommand(c common2.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
    61  	o := &appInitOptions{IOStreams: ioStreams, c: c}
    62  	cmd := &cobra.Command{
    63  		Use:                   "init",
    64  		DisableFlagsInUseLine: true,
    65  		Short:                 "Create scaffold for an application.",
    66  		Long:                  "Create scaffold for vela application.",
    67  		Example:               "vela init",
    68  		RunE: func(cmd *cobra.Command, args []string) error {
    69  			var err error
    70  			o.Namespace, err = GetFlagNamespaceOrEnv(cmd, c)
    71  			if err != nil {
    72  				return err
    73  			}
    74  
    75  			newClient, err := c.GetClient()
    76  			if err != nil {
    77  				return err
    78  			}
    79  			o.client = newClient
    80  			o.IOStreams.Info("Welcome to use KubeVela CLI! Please describe your application.")
    81  			o.IOStreams.Info()
    82  			if err = o.CheckEnv(); err != nil {
    83  				return err
    84  			}
    85  			if err = o.Naming(); err != nil {
    86  				return err
    87  			}
    88  			if err = o.Workload(); err != nil {
    89  				return err
    90  			}
    91  
    92  			if err := appfile.Validate(o.app); err != nil {
    93  				return err
    94  			}
    95  
    96  			b, err := yaml.Marshal(o.app.AppFile)
    97  			if err != nil {
    98  				return err
    99  			}
   100  			err = os.WriteFile("./vela.yaml", b, 0600)
   101  			if err != nil {
   102  				return err
   103  			}
   104  			o.IOStreams.Info("\nDeployment config is rendered and written to " + color.New(color.FgCyan).Sprint("vela.yaml"))
   105  
   106  			if o.renderOnly {
   107  				return nil
   108  			}
   109  
   110  			ctx := context.Background()
   111  			err = appfile.BuildRun(ctx, o.app, o.client, o.Namespace, o.IOStreams)
   112  			if err != nil {
   113  				return err
   114  			}
   115  			deployStatus, err := printTrackingDeployStatus(c, o.IOStreams, o.appName, o.Namespace, 300*time.Second)
   116  			if err != nil {
   117  				return err
   118  			}
   119  			if deployStatus != appDeployedHealthy {
   120  				return nil
   121  			}
   122  			return printAppStatus(context.Background(), newClient, ioStreams, o.appName, o.Namespace, cmd, c, false)
   123  		},
   124  		Annotations: map[string]string{
   125  			types.TagCommandOrder: order,
   126  			types.TagCommandType:  types.TypeStart,
   127  		},
   128  	}
   129  	cmd.Flags().BoolVar(&o.renderOnly, "render-only", false, "Rendering vela.yaml in current dir and do not deploy")
   130  	addNamespaceAndEnvArg(cmd)
   131  	cmd.SetOut(ioStreams.Out)
   132  	return cmd
   133  }
   134  
   135  // Naming asks user to input app name
   136  func (o *appInitOptions) Naming() error {
   137  	prompt := &survey.Input{
   138  		Message: "What would you like to name your application (required): ",
   139  	}
   140  	err := survey.AskOne(prompt, &o.appName, survey.WithValidator(survey.Required))
   141  	if err != nil {
   142  		return fmt.Errorf("read app name err %w", err)
   143  	}
   144  	return nil
   145  }
   146  
   147  // CheckEnv checks environment, e.g., domain and email.
   148  func (o *appInitOptions) CheckEnv() error {
   149  	if o.Namespace == "" {
   150  		o.Namespace = "default"
   151  	}
   152  	var ns v1.Namespace
   153  	ctx := context.Background()
   154  	err := o.client.Get(ctx, client.ObjectKey{
   155  		Name: o.Namespace,
   156  	}, &ns)
   157  	if apierrors.IsNotFound(err) {
   158  		ns.Name = o.Namespace
   159  		err = o.client.Create(ctx, &ns)
   160  		if err != nil {
   161  			return err
   162  		}
   163  	} else if err != nil {
   164  		return err
   165  	}
   166  	return nil
   167  }
   168  
   169  func formatAndGetUsage(p *types.Parameter) string {
   170  	usage := p.Usage
   171  	if usage == "" {
   172  		usage = "what would you configure for parameter '" + color.New(color.FgCyan).Sprintf("%s", p.Name) + "'"
   173  	}
   174  	if p.Required {
   175  		usage += " (required): "
   176  	} else {
   177  		defaultValue := fmt.Sprintf("%v", p.Default)
   178  		if defaultValue != "" {
   179  			usage += fmt.Sprintf(" (optional, default is %s): ", defaultValue)
   180  		} else {
   181  			usage += " (optional): "
   182  		}
   183  		if val, ok := p.Default.(json.Number); ok {
   184  			if p.Type == cue.NumberKind || p.Type == cue.FloatKind {
   185  				p.Default, _ = val.Float64()
   186  			}
   187  			if p.Type == cue.IntKind {
   188  				p.Default, _ = val.Int64()
   189  			}
   190  		}
   191  	}
   192  	return usage
   193  }
   194  
   195  // Workload asks user to choose workload type from installed workloads
   196  func (o *appInitOptions) Workload() error {
   197  	workloads, err := docgen.LoadInstalledCapabilityWithType(o.Namespace, o.c, types.TypeComponentDefinition)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	var workloadList []string
   202  	for _, w := range workloads {
   203  		workloadList = append(workloadList, w.Name)
   204  	}
   205  	prompt := &survey.Select{
   206  		Message: "Choose the workload type for your application (required, e.g., webservice): ",
   207  		Options: workloadList,
   208  	}
   209  	err = survey.AskOne(prompt, &o.workloadType, survey.WithValidator(survey.Required))
   210  	if err != nil {
   211  		return fmt.Errorf("read workload type err %w", err)
   212  	}
   213  	workload, err := GetCapabilityByName(o.workloadType, workloads)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	namePrompt := &survey.Input{
   218  		Message: fmt.Sprintf("What would you like to name this %s (required): ", o.workloadType),
   219  	}
   220  	err = survey.AskOne(namePrompt, &o.workloadName, survey.WithValidator(survey.Required))
   221  	if err != nil {
   222  		return fmt.Errorf("read workload name err %w", err)
   223  	}
   224  	fs := pflag.NewFlagSet("workload", pflag.ContinueOnError)
   225  	for _, pp := range workload.Parameters {
   226  		p := pp
   227  		if p.Name == "name" {
   228  			continue
   229  		}
   230  		if p.Ignore {
   231  			continue
   232  		}
   233  		usage := formatAndGetUsage(&p)
   234  		// nolint:exhaustive
   235  		switch p.Type {
   236  		case cue.StringKind:
   237  			var data string
   238  			prompt := &survey.Input{
   239  				Message: usage,
   240  			}
   241  			var opts []survey.AskOpt
   242  			if p.Required {
   243  				opts = append(opts, survey.WithValidator(survey.Required))
   244  			}
   245  			err = survey.AskOne(prompt, &data, opts...)
   246  			if err != nil {
   247  				return fmt.Errorf("read param %s err %w", p.Name, err)
   248  			}
   249  			fs.String(p.Name, data, p.Usage)
   250  		case cue.NumberKind, cue.FloatKind:
   251  			var data string
   252  			prompt := &survey.Input{
   253  				Message: usage,
   254  			}
   255  			var opts []survey.AskOpt
   256  			if p.Required {
   257  				opts = append(opts, survey.WithValidator(survey.Required))
   258  			}
   259  			opts = append(opts, survey.WithValidator(func(ans interface{}) error {
   260  				data := ans.(string)
   261  				if data == "" && !p.Required {
   262  					return nil
   263  				}
   264  				_, err := strconv.ParseFloat(data, 64)
   265  				return err
   266  			}))
   267  			err = survey.AskOne(prompt, &data, opts...)
   268  			if err != nil {
   269  				return fmt.Errorf("read param %s err %w", p.Name, err)
   270  			}
   271  			if data == "" {
   272  				fs.Float64(p.Name, p.Default.(float64), p.Usage)
   273  			} else {
   274  				val, _ := strconv.ParseFloat(data, 64)
   275  				fs.Float64(p.Name, val, p.Usage)
   276  			}
   277  		case cue.IntKind:
   278  			var data string
   279  			prompt := &survey.Input{
   280  				Message: usage,
   281  			}
   282  			var opts []survey.AskOpt
   283  			if p.Required {
   284  				opts = append(opts, survey.WithValidator(survey.Required))
   285  			}
   286  			opts = append(opts, survey.WithValidator(func(ans interface{}) error {
   287  				data := ans.(string)
   288  				if data == "" && !p.Required {
   289  					return nil
   290  				}
   291  				_, err := strconv.ParseInt(data, 10, 64)
   292  				return err
   293  			}))
   294  			err = survey.AskOne(prompt, &data, opts...)
   295  			if err != nil {
   296  				return fmt.Errorf("read param %s err %w", p.Name, err)
   297  			}
   298  			if data == "" {
   299  				fs.Int64(p.Name, p.Default.(int64), p.Usage)
   300  			} else {
   301  				val, _ := strconv.ParseInt(data, 10, 64)
   302  				fs.Int64(p.Name, val, p.Usage)
   303  			}
   304  		case cue.BoolKind:
   305  			var data bool
   306  			prompt := &survey.Confirm{
   307  				Message: usage,
   308  			}
   309  			if p.Required {
   310  				err = survey.AskOne(prompt, &data, survey.WithValidator(survey.Required))
   311  			} else {
   312  				err = survey.AskOne(prompt, &data)
   313  			}
   314  			if err != nil {
   315  				return fmt.Errorf("read param %s err %w", p.Name, err)
   316  			}
   317  			fs.Bool(p.Name, data, p.Usage)
   318  		default:
   319  			// other type not supported
   320  		}
   321  	}
   322  	o.app, err = common.BaseComplete(o.Namespace, o.c, o.workloadName, o.appName, fs, o.workloadType)
   323  	return err
   324  }
   325  
   326  // GetCapabilityByName get eponymous types.Capability from workloads by name
   327  func GetCapabilityByName(name string, workloads []types.Capability) (types.Capability, error) {
   328  	for _, v := range workloads {
   329  		if v.Name == name {
   330  			return v, nil
   331  		}
   332  	}
   333  	return types.Capability{}, fmt.Errorf("%s not found", name)
   334  }