github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/cmd/tk/env.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"text/tabwriter"
    10  
    11  	"k8s.io/utils/strings/slices"
    12  
    13  	"github.com/go-clix/cli"
    14  	"github.com/pkg/errors"
    15  	"github.com/posener/complete"
    16  
    17  	"github.com/grafana/tanka/pkg/jsonnet/jpath"
    18  	"github.com/grafana/tanka/pkg/kubernetes/client"
    19  	"github.com/grafana/tanka/pkg/spec/v1alpha1"
    20  	"github.com/grafana/tanka/pkg/tanka"
    21  	"github.com/grafana/tanka/pkg/term"
    22  )
    23  
    24  func envCmd() *cli.Command {
    25  	cmd := &cli.Command{
    26  		Use:   "env [action]",
    27  		Short: "manipulate environments",
    28  		Args:  cli.ArgsMin(1), // Make sure we print out the help if no subcommand is given, `tk env` is not valid
    29  	}
    30  
    31  	addCommandsWithLogLevelOption(
    32  		cmd,
    33  		envAddCmd(),
    34  		envSetCmd(),
    35  		envListCmd(),
    36  		envRemoveCmd(),
    37  	)
    38  
    39  	return cmd
    40  }
    41  
    42  var kubectlContexts = cli.PredictFunc(
    43  	func(complete.Args) []string {
    44  		c, _ := client.Contexts()
    45  		return c
    46  	},
    47  )
    48  
    49  func envSetCmd() *cli.Command {
    50  	cmd := &cli.Command{
    51  		Use:   "set <path>",
    52  		Short: "update properties of an environment",
    53  		Args:  workflowArgs,
    54  		Predictors: complete.Flags{
    55  			"server-from-context": kubectlContexts,
    56  		},
    57  	}
    58  
    59  	// flags
    60  	tmp := v1alpha1.Environment{}
    61  	envSettingsFlags(&tmp, cmd.Flags())
    62  
    63  	// removed name flag
    64  	name := cmd.Flags().String("name", "", "")
    65  	_ = cmd.Flags().MarkHidden("name")
    66  
    67  	cmd.Run = func(cmd *cli.Command, args []string) error {
    68  		if *name != "" {
    69  			return fmt.Errorf("it looks like you attempted to rename the environment using `--name`. However, this is not possible with Tanka, because the environments name is inferred from the directories name. To rename the environment, rename its directory instead")
    70  		}
    71  
    72  		path, err := filepath.Abs(args[0])
    73  		if err != nil {
    74  			return err
    75  		}
    76  
    77  		if cmd.Flags().Changed("server-from-context") {
    78  			server, err := client.IPFromContext(tmp.Spec.APIServer)
    79  			if err != nil {
    80  				return fmt.Errorf("resolving IP from context: %s", err)
    81  			}
    82  			tmp.Spec.APIServer = server
    83  		}
    84  
    85  		cfg, err := tanka.Peek(path, tanka.Opts{})
    86  		if err != nil {
    87  			return err
    88  		}
    89  
    90  		if tmp.Spec.APIServer != "" && tmp.Spec.APIServer != cfg.Spec.APIServer {
    91  			fmt.Printf("updated spec.apiServer (`%s` -> `%s`)\n", cfg.Spec.APIServer, tmp.Spec.APIServer)
    92  			cfg.Spec.APIServer = tmp.Spec.APIServer
    93  		}
    94  		if tmp.Spec.ContextNames != nil && !slices.Equal(tmp.Spec.ContextNames, cfg.Spec.ContextNames) {
    95  			fmt.Printf("updated spec.contextNames (`%v` -> `%v`)\n", cfg.Spec.ContextNames, tmp.Spec.ContextNames)
    96  			cfg.Spec.ContextNames = tmp.Spec.ContextNames
    97  		}
    98  		if tmp.Spec.Namespace != "" && tmp.Spec.Namespace != cfg.Spec.Namespace {
    99  			fmt.Printf("updated spec.namespace (`%s` -> `%s`)\n", cfg.Spec.Namespace, tmp.Spec.Namespace)
   100  			cfg.Spec.Namespace = tmp.Spec.Namespace
   101  		}
   102  		if tmp.Spec.DiffStrategy != "" && tmp.Spec.DiffStrategy != cfg.Spec.DiffStrategy {
   103  			fmt.Printf("updated spec.diffStrategy (`%s` -> `%s`)\n", cfg.Spec.DiffStrategy, tmp.Spec.DiffStrategy)
   104  			cfg.Spec.DiffStrategy = tmp.Spec.DiffStrategy
   105  		}
   106  		if tmp.Spec.InjectLabels != cfg.Spec.InjectLabels {
   107  			fmt.Printf("updated spec.injectLabels (`%t` -> `%t`)\n", cfg.Spec.InjectLabels, tmp.Spec.InjectLabels)
   108  			cfg.Spec.InjectLabels = tmp.Spec.InjectLabels
   109  		}
   110  
   111  		// This ensures the environment is valid before setting it
   112  		l := tanka.LoadResult{Env: cfg}
   113  		if _, err := l.Connect(); err != nil {
   114  			return err
   115  		}
   116  
   117  		return writeJSON(cfg, filepath.Join(path, "spec.json"))
   118  	}
   119  	return cmd
   120  }
   121  
   122  func envAddCmd() *cli.Command {
   123  	cmd := &cli.Command{
   124  		Use:   "add <path>",
   125  		Short: "create a new environment",
   126  		Args:  cli.ArgsExact(1),
   127  	}
   128  	cfg := v1alpha1.New()
   129  	envSettingsFlags(cfg, cmd.Flags())
   130  	inline := cmd.Flags().BoolP("inline", "i", false, "create an inline environment")
   131  
   132  	cmd.Run = func(cmd *cli.Command, args []string) error {
   133  		if cmd.Flags().Changed("server-from-context") {
   134  			server, err := client.IPFromContext(cfg.Spec.APIServer)
   135  			if err != nil {
   136  				return fmt.Errorf("resolving IP from context: %s", err)
   137  			}
   138  			cfg.Spec.APIServer = server
   139  		}
   140  		// This ensures the environment is valid before adding it
   141  		if cmd.Flags().Changed("server-from-context") || cmd.Flags().Changed("context-name") {
   142  			l := tanka.LoadResult{Env: cfg}
   143  			if _, err := l.Connect(); err != nil {
   144  				return err
   145  			}
   146  		}
   147  
   148  		return addEnv(args[0], cfg, *inline)
   149  	}
   150  	return cmd
   151  }
   152  
   153  // used by initCmd() as well
   154  func addEnv(dir string, cfg *v1alpha1.Environment, inline bool) error {
   155  	path, err := filepath.Abs(dir)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	if _, err := os.Stat(path); err != nil {
   160  		// folder does not exist
   161  		if os.IsNotExist(err) {
   162  			if err := os.MkdirAll(path, os.ModePerm); err != nil {
   163  				return errors.Wrap(err, "creating directory")
   164  			}
   165  		} else {
   166  			// it exists
   167  			if os.IsExist(err) {
   168  				return fmt.Errorf("directory %s already exists", path)
   169  			}
   170  			// we have another error
   171  			return errors.Wrap(err, "creating directory")
   172  		}
   173  	}
   174  
   175  	rootDir, err := jpath.FindRoot(path)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	// the other properties are already set by v1alpha1.New() and pflag.Parse()
   180  	cfg.Metadata.Name, _ = filepath.Rel(rootDir, path)
   181  
   182  	if inline {
   183  		cfg.Data = struct{}{}
   184  		// write main.jsonnet with inline tanka.dev/Environment
   185  		if err := writeJsonnet(cfg, filepath.Join(path, "main.jsonnet")); err != nil {
   186  			return err
   187  		}
   188  	} else {
   189  		// write spec.json
   190  		if err := writeJSON(cfg, filepath.Join(path, "spec.json")); err != nil {
   191  			return err
   192  		}
   193  
   194  		// write main.jsonnet
   195  		if err := writeJsonnet(struct{}{}, filepath.Join(path, "main.jsonnet")); err != nil {
   196  			return err
   197  		}
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  func envRemoveCmd() *cli.Command {
   204  	return &cli.Command{
   205  		Use:     "remove <path>",
   206  		Aliases: []string{"rm"},
   207  		Short:   "delete an environment",
   208  		Args:    workflowArgs,
   209  		Run: func(cmd *cli.Command, args []string) error {
   210  			for _, arg := range args {
   211  				path, err := filepath.Abs(arg)
   212  				if err != nil {
   213  					return fmt.Errorf("parsing environments name: %s", err)
   214  				}
   215  				if err := term.Confirm(fmt.Sprintf("Permanently removing the environment located at '%s'.", path), "yes"); err != nil {
   216  					return err
   217  				}
   218  				if err := os.RemoveAll(path); err != nil {
   219  					return fmt.Errorf("removing '%s': %s", path, err)
   220  				}
   221  				fmt.Println("Removed", path)
   222  			}
   223  			return nil
   224  		},
   225  	}
   226  }
   227  
   228  func envListCmd() *cli.Command {
   229  	args := workflowArgs
   230  	args.Validator = cli.ArgsRange(0, 1)
   231  
   232  	cmd := &cli.Command{
   233  		Use:     "list [<path>]",
   234  		Aliases: []string{"ls"},
   235  		Short:   "list environments relative to current dir or <path>",
   236  		Args:    args,
   237  	}
   238  
   239  	useJSON := cmd.Flags().Bool("json", false, "json output")
   240  	getLabelSelector := labelSelectorFlag(cmd.Flags())
   241  
   242  	useNames := cmd.Flags().Bool("names", false, "plain names output")
   243  
   244  	cmd.Run = func(cmd *cli.Command, args []string) error {
   245  		var path string
   246  		var err error
   247  		if len(args) == 1 {
   248  			path = args[0]
   249  		} else {
   250  			path, err = os.Getwd()
   251  			if err != nil {
   252  				return nil
   253  			}
   254  		}
   255  
   256  		envs, err := tanka.FindEnvs(path, tanka.FindOpts{Selector: getLabelSelector()})
   257  		if err != nil {
   258  			return err
   259  		}
   260  		sort.SliceStable(envs, func(i, j int) bool { return envs[i].Metadata.Name < envs[j].Metadata.Name })
   261  
   262  		if *useJSON {
   263  			j, err := json.Marshal(envs)
   264  			if err != nil {
   265  				return fmt.Errorf("formatting as json: %s", err)
   266  			}
   267  			fmt.Println(string(j))
   268  			return nil
   269  		} else if *useNames {
   270  			for _, e := range envs {
   271  				fmt.Println(e.Metadata.Name)
   272  			}
   273  			return nil
   274  		}
   275  
   276  		w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
   277  		f := "%s\t%s\t%s\t\n"
   278  		fmt.Fprintf(w, f, "NAME", "NAMESPACE", "SERVER")
   279  		for _, e := range envs {
   280  			fmt.Fprintf(w, f, e.Metadata.Name, e.Spec.Namespace, e.Spec.APIServer)
   281  		}
   282  		w.Flush()
   283  
   284  		return nil
   285  	}
   286  	return cmd
   287  }