package hostenv import ( "context" "errors" "flag" "fmt" "io" "os" "path/filepath" ) // carlos system host-env write|seed (region HA spec 2 ยง5 step 2). // // - write is the start-up script's step: as root, it assembles host.env // from the deployment's parameters and writes it 0600. A missing // required value stops it, and the file is left as it was. // - seed is the operator's: it writes a box's existing host.env into // the parameter layout, once per region. const hostEnvUsage = `usage: carlos system host-env write --deployment --region [--aws-region ] carlos system host-env seed --deployment --region --kms-key [--aws-region ] [--from ] write build host.env from the deployment's parameters (root) seed copy a box's host.env into the deployment's parameters ` // hostEnvRoot is the root write's fixed path is under: "/" on a box. // Tests point it at a directory of their own; there is no flag. var hostEnvRoot = "/" // openParams is the parameter store: the AWS adapter's (awsssm.go). // kmsKey is the key seed encrypts with; write passes "". var openParams = openSSMParams // RunSystemHostEnv is `carlos system host-env`. func RunSystemHostEnv(args []string) { if err := runHostEnv(context.Background(), args, os.Stdout, os.Geteuid(), openParams); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func runHostEnv(ctx context.Context, args []string, out io.Writer, euid int, open func(ctx context.Context, awsRegion, kmsKey string) (Params, error)) error { if len(args) == 0 || (args[0] != "write" && args[0] != "seed") { return usageError() } verb := args[0] fs := flag.NewFlagSet("system host-env "+verb, flag.ContinueOnError) fs.SetOutput(io.Discard) deployment := fs.String("deployment", "", "") region := fs.String("region", "", "") awsRegion := fs.String("aws-region", "", "") kmsKey := fs.String("kms-key", "", "") from := fs.String("from", Path, "") if err := fs.Parse(args[1:]); err != nil || fs.NArg() != 0 || *deployment == "" || *region == "" { return usageError() } switch verb { case "write": if euid != 0 { return errors.New("carlos system host-env write runs as root: it writes a root-owned file") } p, err := open(ctx, *awsRegion, "") if err != nil { return err } body, err := Fetch(ctx, p, *deployment, *region) if err != nil { return fmt.Errorf("host.env was not written: %w", err) } // The path is fixed, /etc/carlos/host.env, and the file is written // only through directories proved to be root's alone, then // replaced whole (writeHostEnv). if err := writeHostEnv(hostEnvRoot, body); err != nil { return fmt.Errorf("host.env was not written: %w", err) } fmt.Fprintln(out, wroteFileLine(filepath.Join(hostEnvRoot, Path), *region)) return nil default: if *kmsKey == "" { return usageError() } body, err := os.ReadFile(*from) if err != nil { return err } env, err := Parse(body) if err != nil { return fmt.Errorf("%s: %w", *from, err) } p, err := open(ctx, *awsRegion, *kmsKey) if err != nil { return err } s, err := Seed(ctx, p, *deployment, *region, env) if err != nil { return fmt.Errorf("the parameters were not all written: %w", err) } fmt.Fprintln(out, seededLine(s)) return nil } } func usageError() error { return errors.New(hostEnvUsage) } // wroteFileLine is what write says once host.env is in place. func wroteFileLine(path, region string) string { return fmt.Sprintf("wrote %s from the parameters for %s", path, region) } // seededLine is what seed says once the parameters are written. func seededLine(s Seeded) string { return fmt.Sprintf("wrote %d parameters under %s and %d under %s", s.Regional, s.RegionPrefix, s.Shared, s.SharedPrefix) }