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  }