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  }