github.com/letsencrypt/boulder@v0.20251208.0/cmd/admin/main.go (about)

     1  // Package main provides the "admin" tool, which can perform various
     2  // administrative actions (such as revoking certificates) against a Boulder
     3  // deployment.
     4  //
     5  // Run "admin -h" for a list of flags and subcommands.
     6  //
     7  // Note that the admin tool runs in "dry-run" mode *by default*. All commands
     8  // which mutate the database (either directly or via gRPC requests) will refuse
     9  // to do so, and instead print log lines representing the work they would do,
    10  // unless the "-dry-run=false" flag is passed.
    11  package main
    12  
    13  import (
    14  	"context"
    15  	"flag"
    16  	"fmt"
    17  	"os"
    18  	"strings"
    19  
    20  	"github.com/letsencrypt/boulder/cmd"
    21  	"github.com/letsencrypt/boulder/features"
    22  )
    23  
    24  type Config struct {
    25  	Admin struct {
    26  		// DB controls the admin tool's direct connection to the database.
    27  		DB cmd.DBConfig
    28  		// TLS controls the TLS client the admin tool uses for gRPC connections.
    29  		TLS cmd.TLSConfig
    30  
    31  		RAService *cmd.GRPCClientConfig
    32  		SAService *cmd.GRPCClientConfig
    33  
    34  		Features features.Config
    35  	}
    36  
    37  	Syslog        cmd.SyslogConfig
    38  	OpenTelemetry cmd.OpenTelemetryConfig
    39  }
    40  
    41  // subcommand specifies the set of methods that a struct must implement to be
    42  // usable as an admin subcommand.
    43  type subcommand interface {
    44  	// Desc should return a short (one-sentence) description of the subcommand for
    45  	// use in help/usage strings.
    46  	Desc() string
    47  	// Flags should register command line flags on the provided flagset. These
    48  	// should use the "TypeVar" methods on the provided flagset, targeting fields
    49  	// on the subcommand struct, so that the results of command line parsing can
    50  	// be used by other methods on the struct.
    51  	Flags(*flag.FlagSet)
    52  	// Run should do all of the subcommand's heavy lifting, with behavior gated on
    53  	// the subcommand struct's member fields which have been populated from the
    54  	// command line. The provided admin object can be used for access to external
    55  	// services like the RA, SA, and configured logger.
    56  	Run(context.Context, *admin) error
    57  }
    58  
    59  // main is the entry-point for the admin tool. We do not include admin in the
    60  // suite of tools which are subcommands of the "boulder" binary, since it
    61  // should be small and portable and standalone.
    62  func main() {
    63  	// Do setup as similarly as possible to all other boulder services, including
    64  	// config parsing and stats and logging setup. However, the one downside of
    65  	// not being bundled with the boulder binary is that we don't get config
    66  	// validation for free.
    67  	defer cmd.AuditPanic()
    68  
    69  	// This is the registry of all subcommands that the admin tool can run.
    70  	subcommands := map[string]subcommand{
    71  		"revoke-cert":            &subcommandRevokeCert{},
    72  		"block-key":              &subcommandBlockKey{},
    73  		"pause-identifier":       &subcommandPauseIdentifier{},
    74  		"unpause-account":        &subcommandUnpauseAccount{},
    75  		"import-limit-overrides": &subcommandImportOverrides{},
    76  		"dump-limit-overrides":   &subcommandDumpEnabledOverrides{},
    77  		"toggle-limit-override":  &subcommandToggleOverride{},
    78  		"add-limit-override":     &subcommandAddOverride{},
    79  	}
    80  
    81  	defaultUsage := flag.Usage
    82  	flag.Usage = func() {
    83  		defaultUsage()
    84  		fmt.Printf("\nSubcommands:\n")
    85  		for name, command := range subcommands {
    86  			fmt.Printf("  %s\n", name)
    87  			fmt.Printf("\t%s\n", command.Desc())
    88  		}
    89  		fmt.Print("\nYou can run \"admin <subcommand> -help\" to get usage for that subcommand.\n")
    90  	}
    91  
    92  	// Start by parsing just the global flags before we get to the subcommand, if
    93  	// they're present.
    94  	configFile := flag.String("config", "", "Path to the configuration file for this service (required)")
    95  	dryRun := flag.Bool("dry-run", true, "Print actions instead of mutating the database")
    96  	flag.Parse()
    97  
    98  	// Figure out which subcommand they want us to run.
    99  	unparsedArgs := flag.Args()
   100  	if len(unparsedArgs) == 0 {
   101  		flag.Usage()
   102  		os.Exit(1)
   103  	}
   104  
   105  	subcommand, ok := subcommands[unparsedArgs[0]]
   106  	if !ok {
   107  		flag.Usage()
   108  		os.Exit(1)
   109  	}
   110  
   111  	// Then parse the rest of the args according to the selected subcommand's
   112  	// flags, and allow the global flags to be placed after the subcommand name.
   113  	subflags := flag.NewFlagSet(unparsedArgs[0], flag.ExitOnError)
   114  	subcommand.Flags(subflags)
   115  	flag.VisitAll(func(f *flag.Flag) {
   116  		// For each flag registered at the global/package level, also register it on
   117  		// the subflags FlagSet. The `f.Value` here is a pointer to the same var
   118  		// that the original global flag would populate, so the same variable can
   119  		// be set either way.
   120  		subflags.Var(f.Value, f.Name, f.Usage)
   121  	})
   122  	_ = subflags.Parse(unparsedArgs[1:])
   123  
   124  	// With the flags all parsed, now we can parse our config and set up our admin
   125  	// object.
   126  	if *configFile == "" {
   127  		flag.Usage()
   128  		os.Exit(1)
   129  	}
   130  
   131  	a, err := newAdmin(*configFile, *dryRun)
   132  	cmd.FailOnError(err, "creating admin object")
   133  
   134  	// Finally, run the selected subcommand.
   135  	if a.dryRun {
   136  		a.log.AuditInfof("admin tool executing a dry-run with the following arguments: %q", strings.Join(os.Args, " "))
   137  	} else {
   138  		a.log.AuditInfof("admin tool executing with the following arguments: %q", strings.Join(os.Args, " "))
   139  	}
   140  
   141  	err = subcommand.Run(context.Background(), a)
   142  	cmd.FailOnError(err, "executing subcommand")
   143  
   144  	if a.dryRun {
   145  		a.log.AuditInfof("admin tool has successfully completed executing a dry-run with the following arguments: %q", strings.Join(os.Args, " "))
   146  		a.log.Info("Dry run complete. Pass -dry-run=false to mutate the database.")
   147  	} else {
   148  		a.log.AuditInfof("admin tool has successfully completed executing with the following arguments: %q", strings.Join(os.Args, " "))
   149  	}
   150  }