chore(gonf): replace global variables with an env struct

This commit is contained in:
Julien Dessaux 2024-06-12 23:24:52 +02:00
parent 07db4ab5bd
commit da6565d5f7
Signed by: adyxax
GPG key ID: F92E51B86E07177E
6 changed files with 105 additions and 112 deletions

View file

@ -1,38 +1,31 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
) )
func cmdBuild(ctx context.Context, func (env *Env) cmdBuild() error {
f *flag.FlagSet, env.flagSet.Init(`gonf build [-FLAG]
args []string,
getenv func(string) string,
stdout, stderr io.Writer,
) error {
f.Init(`gonf build [-FLAG]
where FLAG can be one or more of`, flag.ContinueOnError) where FLAG can be one or more of`, flag.ContinueOnError)
hostFlag := addHostFlag(f) hostFlag := env.addHostFlag()
f.SetOutput(stderr) env.flagSet.SetOutput(env.stderr)
f.Parse(args) env.flagSet.Parse(env.args)
if helpMode { if env.helpMode {
f.SetOutput(stdout) env.flagSet.SetOutput(env.stdout)
f.Usage() env.flagSet.Usage()
} }
hostDir, err := hostFlagToHostDir(hostFlag, getenv) hostDir, err := env.hostFlagToHostDir(hostFlag)
if err != nil { if err != nil {
f.Usage() env.flagSet.Usage()
return err return err
} }
return runBuild(ctx, stderr, hostDir) return env.runBuild(hostDir)
} }
func runBuild(ctx context.Context, stderr io.Writer, hostDir string) error { func (env *Env) runBuild(hostDir string) error {
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
return err return err
@ -41,10 +34,10 @@ func runBuild(ctx context.Context, stderr io.Writer, hostDir string) error {
if err = os.Chdir(hostDir); err != nil { if err = os.Chdir(hostDir); err != nil {
return err return err
} }
cmd := exec.CommandContext(ctx, "go", "build", "-ldflags", "-s -w -extldflags \"-static\"", hostDir) cmd := exec.CommandContext(env.ctx, "go", "build", "-ldflags", "-s -w -extldflags \"-static\"", hostDir)
cmd.Env = append(cmd.Environ(), "CGO_ENABLED=0") cmd.Env = append(cmd.Environ(), "CGO_ENABLED=0")
if out, err := cmd.CombinedOutput(); err != nil { if out, err := cmd.CombinedOutput(); err != nil {
_, _ = fmt.Fprint(stderr, string(out)) _, _ = fmt.Fprint(env.stderr, string(out))
return err return err
} }
return nil return nil

View file

@ -1,43 +1,31 @@
package main package main
import ( import (
"context"
"flag" "flag"
"io"
"log/slog" "log/slog"
"path/filepath" "path/filepath"
) )
func cmdDeploy(ctx context.Context, func (env *Env) cmdDeploy() error {
f *flag.FlagSet, env.flagSet.Init(`gonf deploy [-FLAG]
args []string,
getenv func(string) string,
stdout, stderr io.Writer,
) error {
f.Init(`gonf deploy [-FLAG]
where FLAG can be one or more of`, flag.ContinueOnError) where FLAG can be one or more of`, flag.ContinueOnError)
hostFlag := addHostFlag(f) hostFlag := env.addHostFlag()
f.SetOutput(stderr) env.flagSet.SetOutput(env.stderr)
f.Parse(args) env.flagSet.Parse(env.args)
if helpMode { if env.helpMode {
f.SetOutput(stdout) env.flagSet.SetOutput(env.stdout)
f.Usage() env.flagSet.Usage()
} }
hostDir, err := hostFlagToHostDir(hostFlag, getenv) hostDir, err := env.hostFlagToHostDir(hostFlag)
if err != nil { if err != nil {
f.Usage() env.flagSet.Usage()
return err return err
} }
return runDeploy(ctx, getenv, stdout, stderr, *hostFlag, hostDir) return env.runDeploy(*hostFlag, hostDir)
} }
func runDeploy(ctx context.Context, func (env *Env) runDeploy(hostFlag string, hostDir string) error {
getenv func(string) string, sshc, err := env.newSSHClient(hostFlag + ":22")
stdout, stderr io.Writer,
hostFlag string,
hostDir string,
) error {
sshc, err := newSSHClient(ctx, getenv, hostFlag+":22")
if err != nil { if err != nil {
slog.Error("deploy", "action", "newSshClient", "error", err) slog.Error("deploy", "action", "newSshClient", "error", err)
return err return err
@ -48,7 +36,7 @@ func runDeploy(ctx context.Context,
} }
}() }()
if err = sshc.SendFile(ctx, stdout, stderr, filepath.Join(hostDir, hostFlag)); err != nil { if err = sshc.SendFile(filepath.Join(hostDir, hostFlag)); err != nil {
slog.Error("deploy", "action", "SendFile", "error", err) slog.Error("deploy", "action", "SendFile", "error", err)
} }

19
cmd/gonf/env.go Normal file
View file

@ -0,0 +1,19 @@
package main
import (
"context"
"flag"
"io"
)
type Env struct {
batchMode bool
ctx context.Context
configDir string
flagSet *flag.FlagSet
helpMode bool
args []string
getenv func(string) string
stdout io.Writer
stderr io.Writer
}

View file

@ -1,26 +1,23 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
) )
func addHostFlag(f *flag.FlagSet) *string { func (env *Env) addHostFlag() *string {
return f.String("host", "", "(REQUIRED) a valid $GONF_CONFIG/hosts/ subdirectory (overrides the GONF_HOST environment variable)") return env.flagSet.String("host", "", "(REQUIRED) a valid $GONF_CONFIG/hosts/ subdirectory (overrides the GONF_HOST environment variable)")
} }
func hostFlagToHostDir(hostFlag *string, func (env *Env) hostFlagToHostDir(hostFlag *string) (string, error) {
getenv func(string) string,
) (string, error) {
if *hostFlag == "" { if *hostFlag == "" {
*hostFlag = getenv("GONF_HOST") *hostFlag = env.getenv("GONF_HOST")
if *hostFlag == "" { if *hostFlag == "" {
return "", fmt.Errorf("the GONF_HOST environment variable is unset and the -host FLAG is missing. Please use one or the other") return "", fmt.Errorf("the GONF_HOST environment variable is unset and the -host FLAG is missing. Please use one or the other")
} }
} }
hostDir := filepath.Join(configDir, "hosts", *hostFlag) hostDir := filepath.Join(env.configDir, "hosts", *hostFlag)
if info, err := os.Stat(hostDir); err != nil { if info, err := os.Stat(hostDir); err != nil {
return "", fmt.Errorf("invalid host name %s: %w", *hostFlag, err) return "", fmt.Errorf("invalid host name %s: %w", *hostFlag, err)
} else if !info.IsDir() { } else if !info.IsDir() {

View file

@ -4,80 +4,72 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"os/signal" "os/signal"
) )
var (
batchMode bool
configDir string
helpMode bool
)
func main() { func main() {
if err := run(context.Background(), env := Env{
os.Args, batchMode: false,
os.Getenv, ctx: context.Background(),
//os.Getwd, configDir: "",
//os.Stdin, flagSet: nil,
os.Stdout, helpMode: false,
os.Stderr, args: os.Args,
); err != nil { getenv: os.Getenv,
stdout: os.Stdout,
stderr: os.Stderr,
}
if err := env.run(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err) fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1) os.Exit(1)
} }
} }
func run(ctx context.Context, func (env *Env) run() error {
args []string, ctx, cancel := signal.NotifyContext(env.ctx, os.Interrupt)
getenv func(string) string,
//getwd func() (string, error),
//stdin io.Reader,
stdout, stderr io.Writer,
) error {
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel() defer cancel()
f := flag.NewFlagSet(`gonf COMMAND [-FLAG] env.ctx = ctx
env.flagSet = flag.NewFlagSet(`gonf COMMAND [-FLAG]
where COMMAND is one of: where COMMAND is one of:
* build: build configuration for a host * build: build configuration for a host
* deploy: deploy configuration for a host * deploy: deploy configuration for a host
* help: show contextual help * help: show contextual help
* version: show build version and time * version: show build version and time
where FLAG can be one or more of`, flag.ContinueOnError) where FLAG can be one or more of`, flag.ContinueOnError)
f.BoolVar(&batchMode, "batch", false, "skips all questions and confirmations, using the default (safe) choices each time") env.flagSet.BoolVar(&env.batchMode, "batch", false, "skips all questions and confirmations, using the default (safe) choices each time")
f.BoolVar(&helpMode, "help", false, "show contextual help") env.flagSet.BoolVar(&env.helpMode, "help", false, "show contextual help")
f.StringVar(&configDir, "config", "", "(REQUIRED for most commands) path to a gonf configurations repository (overrides the GONF_CONFIG environment variable)") env.flagSet.StringVar(&env.configDir, "config", "", "(REQUIRED for most commands) path to a gonf configurations repository (overrides the GONF_CONFIG environment variable)")
f.SetOutput(stderr) env.flagSet.SetOutput(env.stderr)
f.Parse(args[1:]) env.flagSet.Parse(env.args[1:])
if f.NArg() < 1 { if env.flagSet.NArg() < 1 {
f.Usage() env.flagSet.Usage()
return fmt.Errorf("no command given") return fmt.Errorf("no command given")
} }
cmd := f.Arg(0) cmd := env.flagSet.Arg(0)
argsTail := f.Args()[1:] env.args = env.flagSet.Args()[1:]
switch cmd { switch cmd {
case "help": case "help":
f.SetOutput(stdout) env.flagSet.SetOutput(env.stdout)
f.Usage() env.flagSet.Usage()
case "version": case "version":
cmdVersion() cmdVersion()
default: default:
if configDir == "" { if env.configDir == "" {
configDir = getenv("GONF_CONFIG") env.configDir = env.getenv("GONF_CONFIG")
if configDir == "" { if env.configDir == "" {
f.Usage() env.flagSet.Usage()
return fmt.Errorf("the GONF_CONFIG environment variable is unset and the -config FLAG is missing. Please use one or the other") return fmt.Errorf("the GONF_CONFIG environment variable is unset and the -config FLAG is missing. Please use one or the other")
} }
} }
switch cmd { switch cmd {
case "build": case "build":
return cmdBuild(ctx, f, argsTail, getenv, stdout, stderr) return env.cmdBuild()
case "deploy": case "deploy":
return cmdDeploy(ctx, f, argsTail, getenv, stdout, stderr) return env.cmdDeploy()
default: default:
f.Usage() env.flagSet.Usage()
return fmt.Errorf("invalid command: %s", cmd) return fmt.Errorf("invalid command: %s", cmd)
} }
} }

View file

@ -18,22 +18,30 @@ import (
type sshClient struct { type sshClient struct {
agentConn net.Conn agentConn net.Conn
client *ssh.Client client *ssh.Client
ctx context.Context
getenv func(string) string
stdout io.Writer
stderr io.Writer
} }
func newSSHClient(context context.Context, func (env *Env) newSSHClient(destination string) (*sshClient, error) {
getenv func(string) string, sshc := sshClient{
destination string, agentConn: nil,
) (*sshClient, error) { client: nil,
var sshc sshClient ctx: env.ctx,
getenv: env.getenv,
stdout: env.stdout,
stderr: env.stderr,
}
var err error var err error
socket := getenv("SSH_AUTH_SOCK") socket := sshc.getenv("SSH_AUTH_SOCK")
if sshc.agentConn, err = net.Dial("unix", socket); err != nil { if sshc.agentConn, err = net.Dial("unix", socket); err != nil {
return nil, fmt.Errorf("failed to open SSH_AUTH_SOCK: %w", err) return nil, fmt.Errorf("failed to open SSH_AUTH_SOCK: %w", err)
} }
agentClient := agent.NewClient(sshc.agentConn) agentClient := agent.NewClient(sshc.agentConn)
hostKeyCallback, err := knownhosts.New(filepath.Join(getenv("HOME"), ".ssh/known_hosts")) hostKeyCallback, err := knownhosts.New(filepath.Join(sshc.getenv("HOME"), ".ssh/known_hosts"))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create hostkeycallback function: %w", err) return nil, fmt.Errorf("failed to create hostkeycallback function: %w", err)
} }
@ -62,11 +70,7 @@ func (sshc *sshClient) Close() error {
return nil return nil
} }
func (sshc *sshClient) SendFile( func (sshc *sshClient) SendFile(filename string) error {
ctx context.Context,
stdout, stderr io.Writer,
filename string,
) error {
session, err := sshc.client.NewSession() session, err := sshc.client.NewSession()
if err != nil { if err != nil {
return fmt.Errorf("failed to create ssh client session: %w", err) return fmt.Errorf("failed to create ssh client session: %w", err)
@ -93,8 +97,8 @@ func (sshc *sshClient) SendFile(
wg.Add(2) wg.Add(2)
errCh := make(chan error, 2) errCh := make(chan error, 2)
session.Stdout = stdout session.Stdout = sshc.stdout
session.Stderr = stderr session.Stderr = sshc.stderr
if err := session.Start("scp -t /usr/local/bin/gonf-run"); err != nil { if err := session.Start("scp -t /usr/local/bin/gonf-run"); err != nil {
return fmt.Errorf("failed to run scp: %w", err) return fmt.Errorf("failed to run scp: %w", err)
} }
@ -124,7 +128,7 @@ func (sshc *sshClient) SendFile(
} }
}() }()
ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ctx, cancel := context.WithTimeout(sshc.ctx, 60*time.Second)
defer cancel() defer cancel()
// wait for all waitgroup.Done() or the timeout // wait for all waitgroup.Done() or the timeout