github.com/snyk/vervet/v6@v6.2.4/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/snyk/vervet/v6/config" 15 "github.com/snyk/vervet/v6/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 }