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 }