github.com/oam-dev/kubevela@v1.9.11/references/cli/kube.go (about) 1 /* 2 Copyright 2022 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 "io" 25 "strings" 26 27 "github.com/pkg/errors" 28 "github.com/spf13/cobra" 29 "gopkg.in/yaml.v3" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/meta" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 apitypes "k8s.io/apimachinery/pkg/types" 37 "k8s.io/kubectl/pkg/util/i18n" 38 "k8s.io/kubectl/pkg/util/templates" 39 "k8s.io/utils/strings/slices" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 42 "github.com/kubevela/workflow/pkg/cue/model/value" 43 44 "github.com/oam-dev/kubevela/apis/types" 45 velacmd "github.com/oam-dev/kubevela/pkg/cmd" 46 cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util" 47 "github.com/oam-dev/kubevela/pkg/multicluster" 48 "github.com/oam-dev/kubevela/pkg/utils" 49 "github.com/oam-dev/kubevela/pkg/utils/util" 50 ) 51 52 // KubeCommandGroup command group for native resource management 53 func KubeCommandGroup(f velacmd.Factory, order string, streams util.IOStreams) *cobra.Command { 54 cmd := &cobra.Command{ 55 Use: "kube", 56 Short: i18n.T("Managing native Kubernetes resources across clusters."), 57 Annotations: map[string]string{ 58 types.TagCommandType: types.TypeAuxiliary, 59 types.TagCommandOrder: order, 60 }, 61 Run: func(cmd *cobra.Command, args []string) { 62 63 }, 64 } 65 cmd.AddCommand(NewKubeApplyCommand(f, streams)) 66 cmd.AddCommand(NewKubeDeleteCommand(f, streams)) 67 return cmd 68 } 69 70 // KubeApplyOptions options for kube apply 71 type KubeApplyOptions struct { 72 files []string 73 clusters []string 74 namespace string 75 dryRun bool 76 77 filesData []utils.FileData 78 objects []*unstructured.Unstructured 79 80 util.IOStreams 81 } 82 83 // Complete . 84 func (opt *KubeApplyOptions) Complete(ctx context.Context) error { 85 var paths []string 86 for _, file := range opt.files { 87 path := strings.TrimSpace(file) 88 if !slices.Contains(paths, path) { 89 paths = append(paths, path) 90 } 91 } 92 for _, path := range paths { 93 data, err := utils.LoadDataFromPath(ctx, path, utils.IsJSONYAMLorCUEFile) 94 if err != nil { 95 return err 96 } 97 opt.filesData = append(opt.filesData, data...) 98 } 99 return nil 100 } 101 102 // Validate will not only validate the args but also read from files and generate the objects 103 func (opt *KubeApplyOptions) Validate() error { 104 if len(opt.files) == 0 { 105 return fmt.Errorf("at least one file should be specified with the --file flag") 106 } 107 if len(opt.filesData) == 0 { 108 return fmt.Errorf("not file found") 109 } 110 if len(opt.clusters) == 0 { 111 opt.clusters = []string{"local"} 112 } 113 jsonObj := func(data []byte, path string) (*unstructured.Unstructured, error) { 114 obj := &unstructured.Unstructured{Object: map[string]interface{}{}} 115 err := json.Unmarshal(data, &obj.Object) 116 if err != nil { 117 return nil, fmt.Errorf("failed to decode object in %s: %w", path, err) 118 } 119 if opt.namespace != "" { 120 obj.SetNamespace(opt.namespace) 121 } else if obj.GetNamespace() == "" { 122 obj.SetNamespace(metav1.NamespaceDefault) 123 } 124 return obj, nil 125 } 126 127 for _, fileData := range opt.filesData { 128 switch { 129 case strings.HasSuffix(fileData.Path, YAMLExtension), strings.HasSuffix(fileData.Path, YMLExtension): 130 decoder := yaml.NewDecoder(bytes.NewReader(fileData.Data)) 131 for { 132 obj := &unstructured.Unstructured{Object: map[string]interface{}{}} 133 err := decoder.Decode(obj.Object) 134 if err != nil { 135 if errors.Is(err, io.EOF) { 136 break 137 } 138 return fmt.Errorf("failed to decode object in %s: %w", fileData.Path, err) 139 } 140 if opt.namespace != "" { 141 obj.SetNamespace(opt.namespace) 142 } else if obj.GetNamespace() == "" { 143 obj.SetNamespace(metav1.NamespaceDefault) 144 } 145 opt.objects = append(opt.objects, obj) 146 } 147 case strings.HasSuffix(fileData.Path, ".json"): 148 obj, err := jsonObj(fileData.Data, fileData.Path) 149 if err != nil { 150 return err 151 } 152 opt.objects = append(opt.objects, obj) 153 case strings.HasSuffix(fileData.Path, ".cue"): 154 val, err := value.NewValue(string(fileData.Data), nil, "") 155 if err != nil { 156 return fmt.Errorf("failed to decode object in %s: %w", fileData.Path, err) 157 } 158 data, err := val.CueValue().MarshalJSON() 159 if err != nil { 160 return fmt.Errorf("failed to marhsal to json for CUE object in %s: %w", fileData.Path, err) 161 } 162 obj, err := jsonObj(data, fileData.Path) 163 if err != nil { 164 return err 165 } 166 opt.objects = append(opt.objects, obj) 167 } 168 } 169 return nil 170 } 171 172 // Run will apply objects to clusters 173 func (opt *KubeApplyOptions) Run(ctx context.Context, cli client.Client) error { 174 if opt.dryRun { 175 for i, obj := range opt.objects { 176 if i > 0 { 177 _, _ = fmt.Fprintf(opt.Out, "---\n") 178 } 179 bs, err := yaml.Marshal(obj.Object) 180 if err != nil { 181 return err 182 } 183 _, _ = opt.Out.Write(bs) 184 } 185 return nil 186 } 187 for i, cluster := range opt.clusters { 188 if i > 0 { 189 _, _ = fmt.Fprintf(opt.Out, "\n") 190 } 191 _, _ = fmt.Fprintf(opt.Out, "Apply objects in cluster %s.\n", cluster) 192 ctx := multicluster.ContextWithClusterName(ctx, cluster) 193 for _, obj := range opt.objects { 194 copiedObj := &unstructured.Unstructured{} 195 bs, err := obj.MarshalJSON() 196 if err != nil { 197 return err 198 } 199 if err = copiedObj.UnmarshalJSON(bs); err != nil { 200 return err 201 } 202 res, err := utils.CreateOrUpdate(ctx, cli, copiedObj) 203 if err != nil { 204 return err 205 } 206 key := strings.TrimPrefix(obj.GetNamespace()+"/"+obj.GetName(), "/") 207 _, _ = fmt.Fprintf(opt.Out, " %s %s %s.\n", obj.GetKind(), key, res) 208 } 209 } 210 return nil 211 } 212 213 var ( 214 kubeApplyLong = templates.LongDesc(i18n.T(` 215 Apply Kubernetes objects in clusters 216 217 Apply Kubernetes objects in multiple clusters. Use --clusters to specify which clusters to 218 apply. If -n/--namespace is used, the original object namespace will be overridden. 219 220 You can use -f/--file to specify the object file/folder to apply. Multiple file inputs are allowed. 221 Directory input and web url input is supported as well. 222 File format can be in YAML, JSON or CUE. 223 `)) 224 225 kubeApplyExample = templates.Examples(i18n.T(` 226 # Apply single object file in managed cluster 227 vela kube apply -f my.yaml --cluster cluster-1 228 229 # Apply object in CUE, the whole CUE file MUST follow the kubernetes API and contain only one object. 230 vela kube apply -f my.cue --cluster cluster-1 231 232 # Apply object in JSON, the whole JSON file MUST follow the kubernetes API and contain only one object. 233 vela kube apply -f my.json --cluster cluster-1 234 235 # Apply multiple object files in multiple managed clusters 236 vela kube apply -f my-1.yaml -f my-2.cue --cluster cluster-1 --cluster cluster-2 237 238 # Apply object file with web url in control plane 239 vela kube apply -f https://raw.githubusercontent.com/kubevela/kubevela/master/docs/examples/app-with-probe/app-with-probe.yaml 240 241 # Apply object files in directory to specified namespace in managed clusters 242 vela kube apply -f ./resources -n demo --cluster cluster-1 --cluster cluster-2 243 244 # Use dry-run to see what will be rendered out in YAML 245 vela kube apply -f my.cue --cluster cluster-1 --dry-run 246 `)) 247 ) 248 249 // NewKubeApplyCommand kube apply command 250 func NewKubeApplyCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command { 251 o := &KubeApplyOptions{IOStreams: streams} 252 cmd := &cobra.Command{ 253 Use: "apply", 254 Short: i18n.T("Apply resources in Kubernetes YAML file to clusters."), 255 Long: kubeApplyLong, 256 Example: kubeApplyExample, 257 Annotations: map[string]string{ 258 types.TagCommandType: types.TypeCD, 259 }, 260 Args: cobra.ExactArgs(0), 261 Run: func(cmd *cobra.Command, args []string) { 262 o.namespace = velacmd.GetNamespace(f, cmd) 263 o.clusters = velacmd.GetClusters(cmd) 264 265 cmdutil.CheckErr(o.Complete(cmd.Context())) 266 cmdutil.CheckErr(o.Validate()) 267 cmdutil.CheckErr(o.Run(cmd.Context(), f.Client())) 268 }, 269 } 270 cmd.Flags().StringSliceVarP(&o.files, "file", "f", o.files, "Files that include native Kubernetes objects to apply.") 271 cmd.Flags().BoolVarP(&o.dryRun, FlagDryRun, "", o.dryRun, "Setting this flag will not apply resources in clusters. It will print out the resource to be applied.") 272 return velacmd.NewCommandBuilder(f, cmd). 273 WithNamespaceFlag( 274 velacmd.NamespaceFlagDisableEnvOption{}, 275 velacmd.UsageOption("The namespace to apply objects. If empty, the namespace declared in the YAML will be used."), 276 ). 277 WithClusterFlag(velacmd.UsageOption("The cluster to apply objects. Setting multiple clusters will apply objects in order.")). 278 WithStreams(streams). 279 WithResponsiveWriter(). 280 Build() 281 } 282 283 // KubeDeleteOptions options for kube delete 284 type KubeDeleteOptions struct { 285 clusters []string 286 namespace string 287 deleteAll bool 288 resource string 289 resourceName string 290 291 util.IOStreams 292 } 293 294 var ( 295 kubeDeleteLong = templates.LongDesc(i18n.T(` 296 Delete Kubernetes objects in clusters 297 298 Delete Kubernetes objects in multiple clusters. Use --clusters to specify which clusters to 299 delete. Use -n/--namespace flags to specify which cluster the target resource locates. 300 301 Use --all flag to delete all this kind of objects in the target namespace and clusters.`)) 302 303 kubeDeleteExample = templates.Examples(i18n.T(` 304 # Delete the deployment nginx in default namespace in cluster-1 305 vela kube delete deployment nginx --cluster cluster-1 306 307 # Delete the deployment nginx in demo namespace in cluster-1 and cluster-2 308 vela kube delete deployment nginx -n demo --cluster cluster-1 --cluster cluster-2 309 310 # Delete all deployments in demo namespace in cluster-1 311 vela kube delete deployment --all -n demo --cluster cluster-1`)) 312 ) 313 314 // Complete . 315 func (opt *KubeDeleteOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) { 316 opt.namespace = velacmd.GetNamespace(f, cmd) 317 if opt.namespace == "" { 318 opt.namespace = metav1.NamespaceDefault 319 } 320 opt.clusters = velacmd.GetClusters(cmd) 321 opt.resource = args[0] 322 if len(args) == 2 { 323 opt.resourceName = args[1] 324 } 325 } 326 327 // Validate . 328 func (opt *KubeDeleteOptions) Validate() error { 329 if opt.resourceName == "" && !opt.deleteAll { 330 return fmt.Errorf("either resource name or flag --all should be set") 331 } 332 if opt.resourceName != "" && opt.deleteAll { 333 return fmt.Errorf("cannot set resource name and flag --all at the same time") 334 } 335 return nil 336 } 337 338 // Run . 339 func (opt *KubeDeleteOptions) Run(f velacmd.Factory, cmd *cobra.Command) error { 340 gvks, err := f.Client().RESTMapper().KindsFor(schema.GroupVersionResource{Resource: opt.resource}) 341 if err != nil { 342 return fmt.Errorf("failed to find kinds for resource %s: %w", opt.resource, err) 343 } 344 if len(gvks) == 0 { 345 return fmt.Errorf("no kinds found for resource %s", opt.resource) 346 } 347 gvk := gvks[0] 348 mappings, err := f.Client().RESTMapper().RESTMappings(gvk.GroupKind(), gvk.Version) 349 if err != nil { 350 return fmt.Errorf("failed to get mappings for resource %s: %w", opt.resource, err) 351 } 352 if len(mappings) == 0 { 353 return fmt.Errorf("no mappings found for resource %s", opt.resource) 354 } 355 mapping := mappings[0] 356 namespaced := mapping.Scope.Name() == meta.RESTScopeNameNamespace 357 for _, cluster := range opt.clusters { 358 ctx := multicluster.ContextWithClusterName(cmd.Context(), cluster) 359 objs, obj := &unstructured.UnstructuredList{}, &unstructured.Unstructured{} 360 objs.SetGroupVersionKind(gvk) 361 obj.SetGroupVersionKind(gvk) 362 switch { 363 case opt.deleteAll && namespaced: 364 err = f.Client().List(ctx, objs, client.InNamespace(opt.namespace)) 365 case opt.deleteAll && !namespaced: 366 err = f.Client().List(ctx, objs) 367 case !opt.deleteAll && namespaced: 368 err = f.Client().Get(ctx, apitypes.NamespacedName{Namespace: opt.namespace, Name: opt.resourceName}, obj) 369 case !opt.deleteAll && !namespaced: 370 err = f.Client().Get(ctx, apitypes.NamespacedName{Name: opt.resourceName}, obj) 371 } 372 if err != nil && !apierrors.IsNotFound(err) && !runtime.IsNotRegisteredError(err) && !meta.IsNoMatchError(err) { 373 return fmt.Errorf("failed to retrieve %s in cluster %s: %w", opt.resource, cluster, err) 374 } 375 for _, toDel := range append(objs.Items, *obj) { 376 key := toDel.GetName() 377 if key == "" { 378 continue 379 } 380 if namespaced { 381 key = toDel.GetNamespace() + "/" + key 382 } 383 if err = f.Client().Delete(ctx, toDel.DeepCopy()); err != nil { 384 return fmt.Errorf("failed to delete %s %s in cluster %s: %w", opt.resource, key, cluster, err) 385 } 386 _, _ = fmt.Fprintf(opt.IOStreams.Out, "%s %s in cluster %s deleted.\n", opt.resource, key, cluster) 387 } 388 } 389 return nil 390 } 391 392 // NewKubeDeleteCommand kube delete command 393 func NewKubeDeleteCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command { 394 o := &KubeDeleteOptions{IOStreams: streams} 395 cmd := &cobra.Command{ 396 Use: "delete", 397 Short: i18n.T("Delete resources in clusters."), 398 Long: kubeDeleteLong, 399 Example: kubeDeleteExample, 400 Annotations: map[string]string{ 401 types.TagCommandType: types.TypeCD, 402 }, 403 Args: cobra.RangeArgs(1, 2), 404 Run: func(cmd *cobra.Command, args []string) { 405 o.Complete(f, cmd, args) 406 cmdutil.CheckErr(o.Validate()) 407 cmdutil.CheckErr(o.Run(f, cmd)) 408 }, 409 } 410 cmd.Flags().BoolVarP(&o.deleteAll, "all", "", o.deleteAll, "Setting this flag will delete all this kind of resources.") 411 return velacmd.NewCommandBuilder(f, cmd). 412 WithNamespaceFlag( 413 velacmd.NamespaceFlagDisableEnvOption{}, 414 velacmd.UsageOption("The namespace to delete objects. If empty, the default namespace will be used."), 415 ). 416 WithClusterFlag(velacmd.UsageOption("The cluster to delete objects. Setting multiple clusters will delete objects in order.")). 417 WithStreams(streams). 418 WithResponsiveWriter(). 419 Build() 420 }