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  }