github.com/oam-dev/kubevela@v1.9.11/references/cli/dryrun.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 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 28 wfv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/client-go/kubernetes/scheme" 31 32 "github.com/pkg/errors" 33 "github.com/spf13/cobra" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 "sigs.k8s.io/yaml" 37 38 apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 39 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 40 corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 41 "github.com/oam-dev/kubevela/apis/types" 42 "github.com/oam-dev/kubevela/pkg/appfile/dryrun" 43 pkgdef "github.com/oam-dev/kubevela/pkg/definition" 44 "github.com/oam-dev/kubevela/pkg/oam" 45 oamutil "github.com/oam-dev/kubevela/pkg/oam/util" 46 "github.com/oam-dev/kubevela/pkg/utils" 47 "github.com/oam-dev/kubevela/pkg/utils/common" 48 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 49 "github.com/oam-dev/kubevela/pkg/workflow/step" 50 ) 51 52 // DryRunCmdOptions contains dry-run cmd options 53 type DryRunCmdOptions struct { 54 cmdutil.IOStreams 55 ApplicationFiles []string 56 DefinitionFile string 57 OfflineMode bool 58 MergeStandaloneFiles bool 59 DefinitionNamespace string 60 } 61 62 // NewDryRunCommand creates `dry-run` command 63 func NewDryRunCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { 64 o := &DryRunCmdOptions{IOStreams: ioStreams} 65 cmd := &cobra.Command{ 66 Use: "dry-run", 67 DisableFlagsInUseLine: true, 68 Short: "Dry Run an application, and output the K8s resources as result to stdout.", 69 Long: `Dry-run application locally, render the Kubernetes resources as result to stdout. 70 vela dry-run -d /definition/directory/or/file/ -f /path/to/app.yaml 71 72 You can also specify a remote url for app: 73 vela dry-run -d /definition/directory/or/file/ -f https://remote-host/app.yaml 74 75 And more, you can specify policy and workflow with application file: 76 vela dry-run -d /definition/directory/or/file/ -f /path/to/app.yaml -f /path/to/policy.yaml -f /path/to/workflow.yaml, OR 77 vela dry-run -d /definition/directory/or/file/ -f /path/to/app.yaml,/path/to/policy.yaml,/path/to/workflow.yaml 78 79 Additionally, if the provided policy and workflow files are not referenced by application file, warning message will show up 80 and those files will be ignored. You can use "merge" flag to make those standalone files effective: 81 vela dry-run -d /definition/directory/or/file/ -f /path/to/app.yaml,/path/to/policy.yaml,/path/to/workflow.yaml --merge 82 83 Limitation: 84 1. Only support one object per file(yaml) for "-f" flag. More support will be added in the future improvement. 85 2. Dry Run with policy and workflow will only take override/topology policies and deploy workflow step into considerations. Other workflow step will be ignored. 86 `, 87 Example: ` 88 # dry-run application 89 vela dry-run -f app.yaml 90 91 # dry-run application with policy and workflow 92 vela dry-run -f app.yaml -f policy.yaml -f workflow.yaml 93 `, 94 Annotations: map[string]string{ 95 types.TagCommandType: types.TypeApp, 96 types.TagCommandOrder: order, 97 }, 98 RunE: func(cmd *cobra.Command, args []string) error { 99 namespace, err := GetFlagNamespaceOrEnv(cmd, c) 100 if err != nil { 101 // We need to return an error only if not in offline mode 102 if !o.OfflineMode { 103 return err 104 } 105 106 // Set the namespace to default to match behavior of `GetFlagNamespaceOrEnv` 107 namespace = types.DefaultAppNamespace 108 } 109 110 buff, err := DryRunApplication(o, c, namespace) 111 if err != nil { 112 return err 113 } 114 o.Info(buff.String()) 115 return nil 116 }, 117 } 118 119 cmd.Flags().StringSliceVarP(&o.ApplicationFiles, "file", "f", []string{"app.yaml"}, "application related file names") 120 cmd.Flags().StringVarP(&o.DefinitionFile, "definition", "d", "", "specify a definition file or directory, it will only be used in dry-run rather than applied to K8s cluster") 121 cmd.Flags().BoolVar(&o.OfflineMode, "offline", false, "Run `dry-run` in offline / local mode, all validation steps will be skipped") 122 cmd.Flags().BoolVar(&o.MergeStandaloneFiles, "merge", false, "Merge standalone files to produce dry-run results") 123 cmd.Flags().StringVarP(&o.DefinitionNamespace, "definition-namespace", "x", "", "Specify which namespace the definition locates. (default \"vela-system\")") 124 addNamespaceAndEnvArg(cmd) 125 cmd.SetOut(ioStreams.Out) 126 return cmd 127 } 128 129 // DryRunApplication will dry-run an application and return the render result 130 func DryRunApplication(cmdOption *DryRunCmdOptions, c common.Args, namespace string) (bytes.Buffer, error) { 131 var err error 132 var buff = bytes.Buffer{} 133 134 var objs []*unstructured.Unstructured 135 if cmdOption.DefinitionFile != "" { 136 objs, err = ReadDefinitionsFromFile(cmdOption.DefinitionFile, cmdOption.IOStreams) 137 if err != nil { 138 return buff, err 139 } 140 } 141 142 // Load a kubernetes client 143 var newClient client.Client 144 if cmdOption.OfflineMode { 145 // We will load a fake client with all the objects present in the definitions file preloaded 146 objs = includeBuiltinWorkflowStepDefinition(objs) 147 newClient, err = c.GetFakeClient(objs) 148 } else { 149 // Load an actual client here 150 newClient, err = c.GetClient() 151 } 152 if err != nil { 153 return buff, err 154 } 155 156 pd, err := c.GetPackageDiscover() 157 if err != nil { 158 return buff, err 159 } 160 config, err := c.GetConfig() 161 if err != nil { 162 return buff, err 163 } 164 165 dryRunOpt := dryrun.NewDryRunOption(newClient, config, pd, objs, false) 166 ctx := oamutil.SetNamespaceInCtx(context.Background(), namespace) 167 ctx = oamutil.SetXDefinitionNamespaceInCtx(ctx, cmdOption.DefinitionNamespace) 168 169 // Perform validation only if not in offline mode 170 if !cmdOption.OfflineMode { 171 for _, applicationFile := range cmdOption.ApplicationFiles { 172 err = dryRunOpt.ValidateApp(ctx, applicationFile) 173 if err != nil { 174 return buff, errors.WithMessagef(err, "validate application: %s by dry-run", applicationFile) 175 } 176 } 177 } 178 179 app, err := readApplicationFromFiles(cmdOption, &buff) 180 if err != nil { 181 return buff, errors.WithMessagef(err, "read application files: %s", cmdOption.ApplicationFiles) 182 } 183 err = dryRunOpt.ExecuteDryRunWithPolicies(ctx, app, &buff) 184 if err != nil { 185 return buff, err 186 } 187 return buff, nil 188 } 189 190 func readObj(path string) (*unstructured.Unstructured, error) { 191 switch { 192 case strings.HasSuffix(path, CUEExtension): 193 def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}} 194 defBytes, err := os.ReadFile(filepath.Clean(path)) 195 if err != nil { 196 return nil, err 197 } 198 if err := def.FromCUEString(string(defBytes), nil); err != nil { 199 return nil, errors.Wrapf(err, "failed to parse CUE for definition") 200 } 201 obj := &unstructured.Unstructured{Object: def.UnstructuredContent()} 202 return obj, nil 203 default: 204 obj := &unstructured.Unstructured{} 205 err := common.ReadYamlToObject(path, obj) 206 if err != nil { 207 return nil, err 208 } 209 return obj, nil 210 } 211 } 212 213 // ReadDefinitionsFromFile will read objects from file or dir in the format of yaml 214 func ReadDefinitionsFromFile(path string, io cmdutil.IOStreams) ([]*unstructured.Unstructured, error) { 215 fi, err := os.Stat(path) 216 if err != nil { 217 return nil, err 218 } 219 if !fi.IsDir() { 220 obj, err := readObj(path) 221 if err != nil { 222 return nil, err 223 } 224 return []*unstructured.Unstructured{obj}, nil 225 } 226 227 var objs []*unstructured.Unstructured 228 err = filepath.WalkDir(path, func(path string, e os.DirEntry, err error) error { 229 if e == nil { 230 io.Errorf("failed to walk nil dir entry %s", path) 231 return nil 232 } 233 if err != nil { 234 io.Errorf("failed to walk dir %s: %v", path, err) 235 return nil 236 } 237 if e.IsDir() { 238 return nil 239 } 240 fileType := filepath.Ext(e.Name()) 241 if fileType != YAMLExtension && fileType != YMLExtension && fileType != CUEExtension { 242 return nil 243 } 244 obj, err := readObj(path) 245 if err != nil { 246 return err 247 } 248 objs = append(objs, obj) 249 return nil 250 }) 251 if err != nil { 252 return nil, err 253 } 254 return objs, nil 255 } 256 257 func readApplicationFromFile(filename string) (*corev1beta1.Application, error) { 258 fileContent, err := utils.ReadRemoteOrLocalPath(filename, true) 259 if err != nil { 260 return nil, err 261 } 262 263 fileType := filepath.Ext(filename) 264 switch fileType { 265 case YAMLExtension, YMLExtension: 266 fileContent, err = yaml.YAMLToJSON(fileContent) 267 if err != nil { 268 return nil, err 269 } 270 } 271 272 app := new(corev1beta1.Application) 273 err = json.Unmarshal(fileContent, app) 274 return app, err 275 } 276 277 func readApplicationFromFiles(cmdOption *DryRunCmdOptions, buff *bytes.Buffer) (*corev1beta1.Application, error) { 278 var app *corev1beta1.Application 279 var policies []*v1alpha1.Policy 280 var wf *wfv1alpha1.Workflow 281 policyNameMap := make(map[string]struct{}) 282 283 for _, filename := range cmdOption.ApplicationFiles { 284 fileContent, err := utils.ReadRemoteOrLocalPath(filename, true) 285 if err != nil { 286 return nil, err 287 } 288 289 fileType := filepath.Ext(filename) 290 switch fileType { 291 case YAMLExtension, YMLExtension: 292 // only support one object in one yaml file 293 fileContent, err = yaml.YAMLToJSON(fileContent) 294 if err != nil { 295 return nil, err 296 } 297 decode := scheme.Codecs.UniversalDeserializer().Decode 298 // cannot guarantee get the object, but gkv is enough 299 _, gkv, _ := decode(fileContent, nil, nil) 300 301 jsonFileContent, err := yaml.YAMLToJSON(fileContent) 302 if err != nil { 303 return nil, err 304 } 305 306 switch *gkv { 307 case corev1beta1.ApplicationKindVersionKind: 308 if app != nil { 309 return nil, errors.New("more than one applications provided") 310 } 311 app = new(corev1beta1.Application) 312 err = json.Unmarshal(jsonFileContent, app) 313 if err != nil { 314 return nil, err 315 } 316 case v1alpha1.PolicyGroupVersionKind: 317 policy := new(v1alpha1.Policy) 318 err = json.Unmarshal(jsonFileContent, policy) 319 if err != nil { 320 return nil, err 321 } 322 policies = append(policies, policy) 323 case v1alpha1.WorkflowGroupVersionKind: 324 if wf != nil { 325 return nil, errors.New("more than one external workflow provided") 326 } 327 wf = new(wfv1alpha1.Workflow) 328 err = json.Unmarshal(jsonFileContent, wf) 329 if err != nil { 330 return nil, err 331 } 332 default: 333 return nil, fmt.Errorf("file %s is not application, policy or workflow", filename) 334 } 335 } 336 } 337 338 // only allow one application 339 if app == nil { 340 return nil, errors.New("no application provided") 341 } 342 343 // workflow not referenced by application 344 if !cmdOption.MergeStandaloneFiles { 345 if wf != nil && 346 ((app.Spec.Workflow != nil && app.Spec.Workflow.Ref != wf.Name) || app.Spec.Workflow == nil) { 347 fmt.Fprintf(buff, "WARNING: workflow %s not referenced by application\n\n", wf.Name) 348 } 349 } else { 350 if wf != nil { 351 app.Spec.Workflow = &corev1beta1.Workflow{ 352 Ref: "", 353 Steps: wf.Steps, 354 } 355 } 356 err := getPolicyNameFromWorkflow(wf, policyNameMap) 357 if err != nil { 358 return nil, err 359 } 360 } 361 362 for _, policy := range policies { 363 // check standalone policies 364 if _, exist := policyNameMap[policy.Name]; !exist && !cmdOption.MergeStandaloneFiles { 365 fmt.Fprintf(buff, "WARNING: policy %s not referenced by application\n\n", policy.Name) 366 continue 367 } 368 app.Spec.Policies = append(app.Spec.Policies, corev1beta1.AppPolicy{ 369 Name: policy.Name, 370 Type: policy.Type, 371 Properties: policy.Properties, 372 }) 373 } 374 return app, nil 375 } 376 377 func getPolicyNameFromWorkflow(wf *wfv1alpha1.Workflow, policyNameMap map[string]struct{}) error { 378 379 checkPolicy := func(wfsb wfv1alpha1.WorkflowStepBase, policyNameMap map[string]struct{}) error { 380 workflowStepSpec := &step.DeployWorkflowStepSpec{} 381 if err := utils.StrictUnmarshal(wfsb.Properties.Raw, workflowStepSpec); err != nil { 382 return err 383 } 384 for _, p := range workflowStepSpec.Policies { 385 policyNameMap[p] = struct{}{} 386 } 387 return nil 388 } 389 390 if wf == nil { 391 return nil 392 } 393 394 for _, wfs := range wf.Steps { 395 if wfs.Type == step.DeployWorkflowStep { 396 err := checkPolicy(wfs.WorkflowStepBase, policyNameMap) 397 if err != nil { 398 return err 399 } 400 for _, sub := range wfs.SubSteps { 401 if sub.Type == step.DeployWorkflowStep { 402 err = checkPolicy(sub, policyNameMap) 403 if err != nil { 404 return err 405 } 406 } 407 } 408 409 } 410 } 411 return nil 412 } 413 414 // includeBuiltinWorkflowStepDefinition adds builtin workflow step definition to the given objects 415 // A few builtin workflow steps have cue definition. They should be included when building offline fake client. 416 func includeBuiltinWorkflowStepDefinition(objs []*unstructured.Unstructured) []*unstructured.Unstructured { 417 deployUnstructured, _ := oamutil.Object2Unstructured(deployDefinition) 418 return append(objs, deployUnstructured) 419 } 420 421 // deployDefinition is the definition of deploy step 422 // Copied it here to make dry-run work in offline mode. 423 var deployDefinition = &corev1beta1.WorkflowStepDefinition{ 424 TypeMeta: metav1.TypeMeta{ 425 Kind: corev1beta1.WorkflowStepDefinitionKind, 426 APIVersion: corev1beta1.SchemeGroupVersion.String(), 427 }, 428 ObjectMeta: metav1.ObjectMeta{ 429 Name: "deploy", 430 Namespace: oam.SystemDefinitionNamespace, 431 }, 432 Spec: corev1beta1.WorkflowStepDefinitionSpec{ 433 Schematic: &apicommon.Schematic{ 434 CUE: &apicommon.CUE{Template: ` 435 import ( 436 "vela/op" 437 ) 438 439 "deploy": { 440 type: "workflow-step" 441 annotations: { 442 "category": "Application Delivery" 443 } 444 labels: { 445 "scope": "Application" 446 } 447 description: "A powerful and unified deploy step for components multi-cluster delivery with policies." 448 } 449 // Ignore the template field for it's useless in dry-run. 450 template: {}`, 451 }, 452 }, 453 }, 454 }