github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/pkg/box/box.go (about) 1 package box 2 3 // box implements the box model. 4 5 import ( 6 "fmt" 7 "log" 8 "os" 9 "path/filepath" 10 "sort" 11 "strings" 12 13 "github.com/StackExchange/blackbox/v2/pkg/bblog" 14 "github.com/StackExchange/blackbox/v2/pkg/bbutil" 15 "github.com/StackExchange/blackbox/v2/pkg/crypters" 16 "github.com/StackExchange/blackbox/v2/pkg/vcs" 17 "github.com/urfave/cli/v2" 18 ) 19 20 var logErr *log.Logger 21 var logDebug *log.Logger 22 23 // Box describes what we know about a box. 24 type Box struct { 25 // Paths: 26 Team string // Name of the team (i.e. .blackbox-$TEAM) 27 RepoBaseDir string // Rel path to the VCS repo. 28 ConfigPath string // Abs or Rel path to the .blackbox (or whatever) directory. 29 ConfigRO bool // True if we should not try to change files in ConfigPath. 30 // Settings: 31 Umask int // umask to set when decrypting 32 Editor string // Editor to call 33 Debug bool // Are we in debug logging mode? 34 // Cache of data gathered from .blackbox: 35 Admins []string // If non-empty, the list of admins. 36 Files []string // If non-empty, the list of files. 37 FilesSet map[string]bool // If non-nil, a set of Files. 38 // Handles to interfaces: 39 Vcs vcs.Vcs // Interface access to the VCS. 40 Crypter crypters.Crypter // Inteface access to GPG. 41 logErr *log.Logger 42 logDebug *log.Logger 43 } 44 45 // StatusMode is a type of query. 46 type StatusMode int 47 48 const ( 49 // Itemized is blah 50 Itemized StatusMode = iota // Individual files by name 51 // All files is blah 52 All 53 // Unchanged is blah 54 Unchanged 55 // Changed is blah 56 Changed 57 ) 58 59 // NewFromFlags creates a box using items from flags. Nearly all subcommands use this. 60 func NewFromFlags(c *cli.Context) *Box { 61 62 // The goal of this is to create a fully-populated box (and box.Vcs) 63 // so that all subcommands have all the fields and interfaces they need 64 // to do their job. 65 66 logErr = bblog.GetErr() 67 logDebug = bblog.GetDebug(c.Bool("debug")) 68 69 bx := &Box{ 70 Umask: c.Int("umask"), 71 Editor: c.String("editor"), 72 Team: c.String("team"), 73 logErr: bblog.GetErr(), 74 logDebug: bblog.GetDebug(c.Bool("debug")), 75 Debug: c.Bool("debug"), 76 } 77 78 // Discover which kind of VCS is in use, and the repo root. 79 bx.Vcs, bx.RepoBaseDir = vcs.Discover() 80 81 // Discover the crypto backend (GnuPG, go-openpgp, etc.) 82 bx.Crypter = crypters.SearchByName(c.String("crypto"), c.Bool("debug")) 83 if bx.Crypter == nil { 84 fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the damn default\n") 85 os.Exit(1) 86 } 87 88 // Find the .blackbox (or equiv.) directory. 89 var err error 90 configFlag := c.String("config") 91 if configFlag != "" { 92 // Flag is set. Better make sure it is valid. 93 if !filepath.IsAbs(configFlag) { 94 fmt.Printf("config flag value is a relative path. Too risky. Exiting.\n") 95 os.Exit(1) 96 // NB(tlim): We could return filepath.Abs(config) or maybe it just 97 // works as is. I don't know, and until we have a use case to prove 98 // it out, it's best to just not implement this. 99 } 100 bx.ConfigPath = configFlag 101 bx.ConfigRO = true // External configs treated as read-only. 102 // TODO(tlim): We could get fancy here and set ConfigReadOnly=true only 103 // if we are sure configFlag is not within bx.RepoBaseDir. Again, I'd 104 // like to see a use-case before we implement this. 105 return bx 106 107 } 108 // Normal path. Flag not set, so we discover the path. 109 bx.ConfigPath, err = FindConfigDir(bx.RepoBaseDir, c.String("team")) 110 if err != nil && c.Command.Name != "info" { 111 fmt.Printf("Can't find .blackbox or equiv. Have you run init?\n") 112 os.Exit(1) 113 } 114 return bx 115 } 116 117 // NewUninitialized creates a box in a pre-init situation. 118 func NewUninitialized(c *cli.Context) *Box { 119 /* 120 This is for "blackbox init" (used before ".blackbox*" exists) 121 122 Init needs: How we populate it: 123 bx.Vcs: Discovered by calling each plug-in until succeeds. 124 bx.ConfigDir: Generated algorithmically (it doesn't exist yet). 125 */ 126 bx := &Box{ 127 Umask: c.Int("umask"), 128 Editor: c.String("editor"), 129 Team: c.String("team"), 130 logErr: bblog.GetErr(), 131 logDebug: bblog.GetDebug(c.Bool("debug")), 132 Debug: c.Bool("debug"), 133 } 134 bx.Vcs, bx.RepoBaseDir = vcs.Discover() 135 if c.String("configdir") == "" { 136 rel := ".blackbox" 137 if bx.Team != "" { 138 rel = ".blackbox-" + bx.Team 139 } 140 bx.ConfigPath = filepath.Join(bx.RepoBaseDir, rel) 141 } else { 142 // Wait. The user is using the --config flag on a repo that 143 // hasn't been created yet? I hope this works! 144 fmt.Printf("ERROR: You can not set --config when initializing a new repo. Please run this command from within a repo, with no --config flag. Or, file a bug explaining your use caseyour use-case. Exiting!\n") 145 os.Exit(1) 146 // TODO(tlim): We could get fancy here and query the Vcs to see if the 147 // path would fall within the repo, figure out the relative path, and 148 // use that value. (and error if configflag is not within the repo). 149 // That would be error prone and would only help the zero users that 150 // ever see the above error message. 151 } 152 return bx 153 } 154 155 // NewForTestingInit creates a box in a bare environment. 156 func NewForTestingInit(vcsname string) *Box { 157 /* 158 159 This is for "blackbox test_init" (secret command used in integration tests; when nothing exists) 160 TestingInitRepo only uses bx.Vcs, so that's all we set. 161 Populates bx.Vcs by finding the provider named vcsname. 162 */ 163 bx := &Box{} 164 165 // Find the 166 var vh vcs.Vcs 167 var err error 168 vcsname = strings.ToLower(vcsname) 169 for _, v := range vcs.Catalog { 170 if strings.ToLower(v.Name) == vcsname { 171 vh, err = v.New() 172 if err != nil { 173 return nil // No idea how that would happen. 174 } 175 } 176 } 177 bx.Vcs = vh 178 179 return bx 180 } 181 182 func (bx *Box) getAdmins() error { 183 // Memoized 184 if len(bx.Admins) != 0 { 185 return nil 186 } 187 188 // TODO(tlim): Try the json file. 189 190 // Try the legacy file: 191 fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt") 192 bx.logDebug.Printf("Admins file: %q", fn) 193 a, err := bbutil.ReadFileLines(fn) 194 if err != nil { 195 return fmt.Errorf("getAdmins can't load %q: %v", fn, err) 196 } 197 if !sort.StringsAreSorted(a) { 198 return fmt.Errorf("file corrupt. Lines not sorted: %v", fn) 199 } 200 bx.Admins = a 201 202 return nil 203 } 204 205 // getFiles populates Files and FileMap. 206 func (bx *Box) getFiles() error { 207 if len(bx.Files) != 0 { 208 return nil 209 } 210 211 // TODO(tlim): Try the json file. 212 213 // Try the legacy file: 214 fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt") 215 bx.logDebug.Printf("Files file: %q", fn) 216 a, err := bbutil.ReadFileLines(fn) 217 if err != nil { 218 return fmt.Errorf("getFiles can't load %q: %v", fn, err) 219 } 220 if !sort.StringsAreSorted(a) { 221 return fmt.Errorf("file corrupt. Lines not sorted: %v", fn) 222 } 223 for _, n := range a { 224 bx.Files = append(bx.Files, filepath.Join(bx.RepoBaseDir, n)) 225 } 226 227 bx.FilesSet = make(map[string]bool, len(bx.Files)) 228 for _, s := range bx.Files { 229 bx.FilesSet[s] = true 230 } 231 232 return nil 233 }