github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/create/create.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 create
    21  
    22  import (
    23  	"context"
    24  	"embed"
    25  	"encoding/json"
    26  	"fmt"
    27  
    28  	"cuelang.org/go/cue"
    29  	"cuelang.org/go/cue/ast"
    30  	"cuelang.org/go/cue/cuecontext"
    31  	cuejson "cuelang.org/go/encoding/json"
    32  	"github.com/leaanthony/debme"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	"k8s.io/apimachinery/pkg/api/meta"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/cli-runtime/pkg/genericclioptions"
    39  	"k8s.io/cli-runtime/pkg/genericiooptions"
    40  	"k8s.io/cli-runtime/pkg/printers"
    41  	"k8s.io/client-go/dynamic"
    42  	"k8s.io/client-go/kubernetes"
    43  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    44  	"k8s.io/kubectl/pkg/scheme"
    45  
    46  	"github.com/1aal/kubeblocks/pkg/cli/edit"
    47  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    48  )
    49  
    50  var (
    51  	//go:embed template/*
    52  	cueTemplate embed.FS
    53  )
    54  
    55  type CreateDependency func(dryRun []string) error
    56  
    57  type DryRunStrategy int
    58  
    59  const (
    60  	DryRunNone DryRunStrategy = iota
    61  	DryRunClient
    62  	DryRunServer
    63  )
    64  
    65  // CreateOptions the options of creation command should inherit baseOptions
    66  type CreateOptions struct {
    67  	Factory   cmdutil.Factory
    68  	Namespace string
    69  
    70  	// Name Resource name of the command line operation
    71  	Name             string
    72  	Args             []string
    73  	Dynamic          dynamic.Interface
    74  	Client           kubernetes.Interface
    75  	Format           printer.Format
    76  	ToPrinter        func(*meta.RESTMapping, bool) (printers.ResourcePrinterFunc, error)
    77  	DryRun           string
    78  	EditBeforeCreate bool
    79  
    80  	// CueTemplateName cue template file name to render the resource
    81  	CueTemplateName string
    82  
    83  	// Options a command options object which extends CreateOptions that will be used
    84  	// to render the cue template
    85  	Options interface{}
    86  
    87  	// GVR is the GroupVersionResource of the resource to be created
    88  	GVR schema.GroupVersionResource
    89  
    90  	// CustomOutPut will be executed after creating successfully.
    91  	CustomOutPut func(options *CreateOptions)
    92  
    93  	// PreCreate optional, make changes on yaml before create
    94  	PreCreate func(*unstructured.Unstructured) error
    95  
    96  	// CleanUpFn will be executed after creating failed.
    97  	CleanUpFn func() error
    98  
    99  	// CreateDependencies will be executed before creating.
   100  	CreateDependencies CreateDependency
   101  
   102  	// Quiet minimize unnecessary output
   103  	Quiet bool
   104  
   105  	genericiooptions.IOStreams
   106  }
   107  
   108  func (o *CreateOptions) Complete() error {
   109  	var err error
   110  	if o.Namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil {
   111  		return err
   112  	}
   113  
   114  	// now we use the first argument as the resource name
   115  	if len(o.Args) > 0 {
   116  		o.Name = o.Args[0]
   117  	}
   118  
   119  	if o.Dynamic, err = o.Factory.DynamicClient(); err != nil {
   120  		return err
   121  	}
   122  
   123  	if o.Client, err = o.Factory.KubernetesClientSet(); err != nil {
   124  		return err
   125  	}
   126  
   127  	o.ToPrinter = func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) {
   128  		var p printers.ResourcePrinter
   129  		switch o.Format {
   130  		case printer.JSON:
   131  			p = &printers.JSONPrinter{}
   132  		case printer.YAML:
   133  			p = &printers.YAMLPrinter{}
   134  		default:
   135  			return nil, genericclioptions.NoCompatiblePrinterError{AllowedFormats: []string{"JSON", "YAML"}}
   136  		}
   137  
   138  		p, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(p, nil)
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  		return p.PrintObj, nil
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  // Run execute command. the options of parameter contain the command flags and args.
   149  func (o *CreateOptions) Run() error {
   150  	resObj, err := o.buildResourceObj()
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	if o.PreCreate != nil {
   156  		if err = o.PreCreate(resObj); err != nil {
   157  			return err
   158  		}
   159  	}
   160  
   161  	if o.EditBeforeCreate {
   162  		customEdit := edit.NewCustomEditOptions(o.Factory, o.IOStreams, "create")
   163  		if err := customEdit.Run(resObj); err != nil {
   164  			return err
   165  		}
   166  	}
   167  
   168  	dryRunStrategy, err := o.GetDryRunStrategy()
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	if dryRunStrategy != DryRunClient {
   174  		createOptions := metav1.CreateOptions{}
   175  
   176  		if dryRunStrategy == DryRunServer {
   177  			createOptions.DryRun = []string{metav1.DryRunAll}
   178  		}
   179  
   180  		// create dependencies
   181  		if o.CreateDependencies != nil {
   182  			if err = o.CreateDependencies(createOptions.DryRun); err != nil {
   183  				return err
   184  			}
   185  		}
   186  
   187  		// create kubernetes resource
   188  		resObj, err = o.Dynamic.Resource(o.GVR).Namespace(o.Namespace).Create(context.TODO(), resObj, createOptions)
   189  		if err != nil {
   190  			if apierrors.IsAlreadyExists(err) {
   191  				return err
   192  			}
   193  
   194  			// for other errors, clean up dependencies
   195  			if cleanErr := o.CleanUp(); cleanErr != nil {
   196  				fmt.Fprintf(o.ErrOut, "Failed to clean up denpendencies: %v\n", cleanErr)
   197  			}
   198  			return err
   199  		}
   200  
   201  		if dryRunStrategy != DryRunServer {
   202  			o.Name = resObj.GetName()
   203  			if o.Quiet {
   204  				return nil
   205  			}
   206  			if o.CustomOutPut != nil {
   207  				o.CustomOutPut(o)
   208  			} else {
   209  				fmt.Fprintf(o.Out, "%s %s created\n", resObj.GetKind(), resObj.GetName())
   210  			}
   211  			return nil
   212  		}
   213  	}
   214  	p, err := o.ToPrinter(nil, false)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	return p.PrintObj(resObj, o.Out)
   219  }
   220  
   221  func (o *CreateOptions) CleanUp() error {
   222  	if o.CreateDependencies == nil {
   223  		return nil
   224  	}
   225  
   226  	if o.CleanUpFn != nil {
   227  		return o.CleanUpFn()
   228  	}
   229  	return nil
   230  }
   231  
   232  func (o *CreateOptions) buildResourceObj() (*unstructured.Unstructured, error) {
   233  	var (
   234  		cueValue    cue.Value
   235  		err         error
   236  		optionsByte []byte
   237  	)
   238  
   239  	if optionsByte, err = json.Marshal(o.Options); err != nil {
   240  		return nil, err
   241  	}
   242  
   243  	// append namespace and name to options and marshal to json
   244  	m := make(map[string]interface{})
   245  	if err = json.Unmarshal(optionsByte, &m); err != nil {
   246  		return nil, err
   247  	}
   248  	m["namespace"] = o.Namespace
   249  	m["name"] = o.Name
   250  	if optionsByte, err = json.Marshal(m); err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	if cueValue, err = newCueValue(o.CueTemplateName); err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	if cueValue, err = fillOptions(cueValue, optionsByte); err != nil {
   259  		return nil, err
   260  	}
   261  	return convertContentToUnstructured(cueValue)
   262  }
   263  
   264  func (o *CreateOptions) GetDryRunStrategy() (DryRunStrategy, error) {
   265  	if o.DryRun == "" {
   266  		return DryRunNone, nil
   267  	}
   268  	switch o.DryRun {
   269  	case "client":
   270  		return DryRunClient, nil
   271  	case "server":
   272  		return DryRunServer, nil
   273  	case "unchanged":
   274  		return DryRunClient, nil
   275  	case "none":
   276  		return DryRunNone, nil
   277  	default:
   278  		return DryRunNone, fmt.Errorf(`invalid dry-run value (%v). Must be "none", "server", or "client"`, o.DryRun)
   279  	}
   280  }
   281  
   282  // NewCueValue converts cue template  to cue Value which holds any value like Boolean,Struct,String and more cue type.
   283  func newCueValue(cueTemplateName string) (cue.Value, error) {
   284  	tmplFs, _ := debme.FS(cueTemplate, "template")
   285  	if tmlBytes, err := tmplFs.ReadFile(cueTemplateName); err != nil {
   286  		return cue.Value{}, err
   287  	} else {
   288  		return cuecontext.New().CompileString(string(tmlBytes)), nil
   289  	}
   290  }
   291  
   292  // fillOptions fills options object in cue template file
   293  func fillOptions(cueValue cue.Value, optionsByte []byte) (cue.Value, error) {
   294  	var (
   295  		expr ast.Expr
   296  		err  error
   297  	)
   298  	if expr, err = cuejson.Extract("", optionsByte); err != nil {
   299  		return cue.Value{}, err
   300  	}
   301  	optionsValue := cueValue.Context().BuildExpr(expr)
   302  	cueValue = cueValue.FillPath(cue.ParsePath("options"), optionsValue)
   303  	return cueValue, nil
   304  }
   305  
   306  // convertContentToUnstructured gets content object in cue template file and convert it to Unstructured
   307  func convertContentToUnstructured(cueValue cue.Value) (*unstructured.Unstructured, error) {
   308  	var (
   309  		contentByte     []byte
   310  		err             error
   311  		unstructuredObj = &unstructured.Unstructured{}
   312  	)
   313  	if contentByte, err = cueValue.LookupPath(cue.ParsePath("content")).MarshalJSON(); err != nil {
   314  		return nil, err
   315  	}
   316  	if err = json.Unmarshal(contentByte, &unstructuredObj); err != nil {
   317  		return nil, err
   318  	}
   319  	return unstructuredObj, nil
   320  }