github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/internal/cmd/backstage.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"sort"
    11  
    12  	"github.com/urfave/cli/v2"
    13  
    14  	"github.com/w3security/vervet/v5/config"
    15  	"github.com/w3security/vervet/v5/internal/backstage"
    16  )
    17  
    18  // BackstageCommand is the `vervet backstage` subcommand.
    19  var BackstageCommand = cli.Command{
    20  	Name: "backstage",
    21  	Subcommands: []*cli.Command{{
    22  		Name:  "update-catalog",
    23  		Usage: "Update Backstage catalog-info.yaml with Vervet API versions",
    24  		Flags: []cli.Flag{
    25  			&cli.StringFlag{
    26  				Name:    "config",
    27  				Aliases: []string{"c", "conf"},
    28  				Usage:   "Project configuration file",
    29  			},
    30  		},
    31  		Action: UpdateCatalog,
    32  	}, {
    33  		Name:  "preview-catalog",
    34  		Usage: "Preview changes to Backstage catalog-info.yaml",
    35  		Flags: []cli.Flag{
    36  			&cli.StringFlag{
    37  				Name:    "config",
    38  				Aliases: []string{"c", "conf"},
    39  				Usage:   "Project configuration file",
    40  			},
    41  		},
    42  		Action: PreviewCatalog,
    43  	}, {
    44  		Name:  "check-catalog",
    45  		Usage: "Check for uncommitted changes in Backstage catalog-info.yaml",
    46  		Flags: []cli.Flag{
    47  			&cli.StringFlag{
    48  				Name:    "config",
    49  				Aliases: []string{"c", "conf"},
    50  				Usage:   "Project configuration file",
    51  			},
    52  		},
    53  		Action: CheckCatalog,
    54  	}},
    55  }
    56  
    57  // UpdateCatalog updates the catalog-info.yaml from Vervet versions.
    58  func UpdateCatalog(ctx *cli.Context) error {
    59  	return processCatalog(ctx, nil)
    60  }
    61  
    62  // PreviewCatalog updates the catalog-info.yaml from Vervet versions.
    63  func PreviewCatalog(ctx *cli.Context) error {
    64  	return processCatalog(ctx, os.Stdout)
    65  }
    66  
    67  // CheckCatalog checks whether the catalog-info.yaml or tracked compiled
    68  // versions it references have uncommitted changes. This is primarily useful in
    69  // CI checks to make sure everything is checked into git for Backstage.
    70  func CheckCatalog(ctx *cli.Context) error {
    71  	projectDir, _, err := projectConfig(ctx)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	if st, err := os.Stat(filepath.Join(projectDir, ".git")); err != nil || !st.IsDir() {
    77  		// no git, no problem, just note
    78  		log.Println(projectDir, "does not seem to be tracked in a git repository")
    79  		return nil
    80  	}
    81  
    82  	catalogInfoPath := filepath.Join(projectDir, "catalog-info.yaml")
    83  	fr, err := os.Open(catalogInfoPath)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	defer fr.Close()
    88  
    89  	err = checkUncommittedChanges(catalogInfoPath)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	catalogInfo, err := backstage.LoadCatalogInfo(fr)
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	for _, vervetAPI := range catalogInfo.VervetAPIs {
    99  		specPath := filepath.Join(projectDir, vervetAPI.Spec.Definition.Text)
   100  		if err := checkUncommittedChanges(specPath); err != nil {
   101  			return err
   102  		}
   103  	}
   104  	return nil
   105  }
   106  
   107  func checkUncommittedChanges(path string) error {
   108  	cmd := exec.Command("git", "status", "--porcelain", path)
   109  	out, err := cmd.Output()
   110  	if err != nil {
   111  		log.Println("failed to execute git:", err)
   112  	}
   113  	if len(out) > 0 {
   114  		return fmt.Errorf("%s has uncommitted changes", path)
   115  	}
   116  	return nil
   117  }
   118  
   119  func processCatalog(ctx *cli.Context, w io.Writer) error {
   120  	projectDir, configFile, err := projectConfig(ctx)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	f, err := os.Open(configFile)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	defer f.Close()
   129  	proj, err := config.Load(f)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	catalogInfoPath := filepath.Join(projectDir, "catalog-info.yaml")
   135  	fr, err := os.Open(catalogInfoPath)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	defer fr.Close()
   140  	catalogInfo, err := backstage.LoadCatalogInfo(fr)
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	matchPath := func(path string) bool { return true }
   146  	if st, err := os.Stat(filepath.Join(projectDir, ".git")); err == nil && st.IsDir() {
   147  		matchPath = func(path string) bool {
   148  			cmd := exec.Command("git", "ls-files", path)
   149  			out, err := cmd.Output()
   150  			if err != nil {
   151  				log.Println("failed to execute git to test output path:", err)
   152  				return false
   153  			}
   154  			if len(out) == 0 {
   155  				return false
   156  			}
   157  			return true
   158  		}
   159  	}
   160  
   161  	// range over maps does not specify order and is not guaranteed to be the
   162  	// same from one iteration to the next, stability is important when
   163  	// generating catalog-info to produce reproducible results
   164  	apiNames := []string{}
   165  	for k := range proj.APIs {
   166  		apiNames = append(apiNames, k)
   167  	}
   168  	sort.Strings(apiNames)
   169  	for _, apiName := range apiNames {
   170  		apiConf := proj.APIs[apiName]
   171  		outputPaths := apiConf.Output.ResolvePaths()
   172  		for _, outputPath := range outputPaths {
   173  			outputPath = filepath.Join(projectDir, outputPath)
   174  			if matchPath(outputPath) {
   175  				if err := catalogInfo.LoadVervetAPIs(projectDir, outputPath); err != nil {
   176  					return err
   177  				}
   178  				break
   179  			}
   180  		}
   181  	}
   182  
   183  	if w == nil {
   184  		fw, err := os.Create(catalogInfoPath)
   185  		if err != nil {
   186  			return err
   187  		}
   188  		defer fw.Close()
   189  		w = fw
   190  	}
   191  
   192  	return catalogInfo.Save(w)
   193  }