github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/cmd/tk/tool.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 12 "github.com/go-clix/cli" 13 "github.com/posener/complete" 14 15 "github.com/grafana/tanka/pkg/jsonnet" 16 "github.com/grafana/tanka/pkg/jsonnet/jpath" 17 ) 18 19 func toolCmd() *cli.Command { 20 cmd := &cli.Command{ 21 Short: "handy utilities for working with jsonnet", 22 Use: "tool [command]", 23 Args: cli.ArgsMin(1), // Make sure we print out the help if no subcommand is given, `tk tool` is not valid 24 } 25 26 addCommandsWithLogLevelOption( 27 cmd, 28 jpathCmd(), 29 importsCmd(), 30 importersCmd(), 31 chartsCmd(), 32 ) 33 return cmd 34 } 35 36 func jpathCmd() *cli.Command { 37 cmd := &cli.Command{ 38 Short: "export JSONNET_PATH for use with other jsonnet tools", 39 Use: "jpath <file/dir>", 40 Args: workflowArgs, 41 } 42 43 debug := cmd.Flags().BoolP("debug", "d", false, "show debug info") 44 45 cmd.Run = func(cmd *cli.Command, args []string) error { 46 path := args[0] 47 48 entrypoint, err := jpath.Entrypoint(path) 49 if err != nil { 50 return fmt.Errorf("resolving JPATH: %s", err) 51 } 52 53 jsonnetpath, base, root, err := jpath.Resolve(entrypoint, false) 54 if err != nil { 55 return fmt.Errorf("resolving JPATH: %s", err) 56 } 57 58 if *debug { 59 // log to debug info to stderr 60 fmt.Fprintln(os.Stderr, "main:", entrypoint) 61 fmt.Fprintln(os.Stderr, "rootDir:", root) 62 fmt.Fprintln(os.Stderr, "baseDir:", base) 63 fmt.Fprintln(os.Stderr, "jpath:", jsonnetpath) 64 } 65 66 // print export JSONNET_PATH to stdout 67 fmt.Printf("%s", strings.Join(jsonnetpath, ":")) 68 69 return nil 70 } 71 72 return cmd 73 } 74 75 func importsCmd() *cli.Command { 76 cmd := &cli.Command{ 77 Use: "imports <path>", 78 Short: "list all transitive imports of an environment", 79 Args: workflowArgs, 80 } 81 82 check := cmd.Flags().StringP("check", "c", "", "git commit hash to check against") 83 84 cmd.Run = func(cmd *cli.Command, args []string) error { 85 var modFiles []string 86 if *check != "" { 87 var err error 88 modFiles, err = gitChangedFiles(*check) 89 if err != nil { 90 return fmt.Errorf("invoking git: %s", err) 91 } 92 } 93 94 path, err := filepath.Abs(args[0]) 95 if err != nil { 96 return fmt.Errorf("loading environment: %s", err) 97 } 98 99 deps, err := jsonnet.TransitiveImports(path) 100 if err != nil { 101 return fmt.Errorf("resolving imports: %s", err) 102 } 103 104 root, err := gitRoot() 105 if err != nil { 106 return fmt.Errorf("invoking git: %s", err) 107 } 108 if modFiles != nil { 109 for _, m := range modFiles { 110 mod := filepath.Join(root, m) 111 if err != nil { 112 return err 113 } 114 115 for _, dep := range deps { 116 if mod == dep { 117 fmt.Printf("Rebuild required. File `%s` imports `%s`, which has been changed in `%s`.\n", args[0], dep, *check) 118 os.Exit(16) 119 } 120 } 121 } 122 fmt.Printf("Rebuild not required, because no imported files have been changed in `%s`.\n", *check) 123 os.Exit(0) 124 } 125 126 s, err := json.Marshal(deps) 127 if err != nil { 128 return fmt.Errorf("formatting: %s", err) 129 } 130 fmt.Println(string(s)) 131 132 return nil 133 } 134 135 return cmd 136 } 137 138 func importersCmd() *cli.Command { 139 cmd := &cli.Command{ 140 Use: "importers <file> <file...> <deleted:file...>", 141 Short: "list all environments that either directly or transitively import the given files", 142 Long: `list all environments that either directly or transitively import the given files 143 If the file being looked up was deleted, it should be prefixed with "deleted:". 144 145 As optimization, 146 if the file is not a vendored (located at <tk-root>/vendor/) or a lib file (located at <tk-root>/lib/), we assume: 147 - it is used in a Tanka environment 148 - it will not be imported by any lib or vendor files 149 - the environment base (closest main file in parent dirs) will be considered an importer 150 - if no base is found, all main files in child dirs will be considered importers 151 `, 152 Args: cli.Args{ 153 Validator: cli.ArgsMin(1), 154 Predictor: complete.PredictFiles("*"), 155 }, 156 } 157 158 root := cmd.Flags().String("root", ".", "root directory to search for environments") 159 cmd.Run = func(cmd *cli.Command, args []string) error { 160 root, err := filepath.Abs(*root) 161 if err != nil { 162 return fmt.Errorf("resolving root: %w", err) 163 } 164 165 for _, f := range args { 166 if strings.HasPrefix(f, "deleted:") { 167 continue 168 } 169 if _, err := os.Stat(f); os.IsNotExist(err) { 170 return fmt.Errorf("file %q does not exist", f) 171 } 172 } 173 174 envs, err := jsonnet.FindImporterForFiles(root, args) 175 if err != nil { 176 return fmt.Errorf("resolving imports: %s", err) 177 } 178 179 fmt.Println(strings.Join(envs, "\n")) 180 181 return nil 182 } 183 184 return cmd 185 } 186 187 func gitRoot() (string, error) { 188 s, err := git("rev-parse", "--show-toplevel") 189 return strings.TrimRight(s, "\n"), err 190 } 191 192 func gitChangedFiles(sha string) ([]string, error) { 193 f, err := git("diff-tree", "--no-commit-id", "--name-only", "-r", sha) 194 if err != nil { 195 return nil, err 196 } 197 return strings.Split(f, "\n"), nil 198 } 199 200 func git(argv ...string) (string, error) { 201 cmd := exec.Command("git", argv...) 202 cmd.Stderr = os.Stderr 203 var buf bytes.Buffer 204 cmd.Stdout = &buf 205 if err := cmd.Run(); err != nil { 206 return "", err 207 } 208 return buf.String(), nil 209 }