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 }