first commit

This commit is contained in:
Adrien PONSIN 2025-04-08 09:03:00 +02:00
commit 6db32797dc
No known key found for this signature in database
GPG Key ID: 7B4D4A32C05C475E
10 changed files with 815 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# middleman
Securely mount the Docker socket: apply fine-grained access control to Docker socket HTTP requests.

35
alias.go Normal file
View File

@ -0,0 +1,35 @@
package middleman
import (
"strings"
)
var aliases = make(map[string]map[string]struct{})
func PluckAlias(parent, flag string) []string {
if _, ok := aliases[parent]; !ok {
aliases[parent] = make(map[string]struct{})
}
var alias string
for word := range strings.SplitAfterSeq(flag, "-") {
alias = word[:1]
if alias == "h" {
alias = "H"
if _, ok := aliases[parent][alias]; !ok {
aliases[parent][alias] = struct{}{}
return []string{alias}
}
}
if _, ok := aliases[parent][alias]; !ok {
aliases[parent][alias] = struct{}{}
return []string{alias}
} else {
alias = strings.ToUpper(alias)
if _, ok = aliases[parent][alias]; !ok {
aliases[parent][alias] = struct{}{}
return []string{alias}
}
}
}
return nil
}

68
cmd/middleman/main.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"context"
"crypto/sha256"
"gitea.illuad.fr/adrien/middleman/command"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup"
"os"
"os/signal"
"runtime/debug"
"syscall"
)
type noCommit struct {
message []byte
}
func (nc noCommit) String() string {
if nc.message == nil {
nc.message = []byte("not tied to a commit") // 6344da387a21711b36a91c2fd55f48a026b2be8b5345cbbf32a31f5c089f4d75
}
sum := sha256.Sum256(nc.message)
return string(sum[:])
}
const appName = "middleman"
var (
version = "dev"
commit = noCommit{}.String()
)
func main() {
info, ok := debug.ReadBuildInfo()
if ok {
if info.Main.Version != "" {
version = info.Main.Version
commit = info.Main.Sum
}
}
// Note: os.Kill/syscall.SIGKILL (SIGKILL) signal cannot be caught or ignored.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
var group *errgroup.Group
group, ctx = errgroup.WithContext(ctx)
app := cli.Command{
Name: appName,
Usage: "Securely mount the Docker socket: apply fine-grained access control to Docker socket HTTP requests",
Version: version + "-" + commit,
DefaultCommand: command.ServeCmd,
Commands: []*cli.Command{
command.Serve(group),
command.Healthcheck(group),
},
Authors: []any{
"Adrien <contact@illuad.fr>",
},
UseShortOptionHandling: true,
}
group.Go(func() error {
return app.Run(ctx, os.Args)
})
if err := group.Wait(); err != nil {
log.Err(err).Send()
}
}

34
command/command.go Normal file
View File

@ -0,0 +1,34 @@
package command
import (
"context"
"gitea.illuad.fr/adrien/middleman/flag"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"os"
"time"
)
const EnvVarPrefix = "MIDDLEMAN"
// before is executed before any subcommands are run.
func before(ctx context.Context, command *cli.Command) (context.Context, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
const humanFormatKeyword = "hf"
if command.String(flag.LogFormatFlagName) == humanFormatKeyword {
log.Logger = zerolog.New(zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
}).With().Timestamp().Logger()
}
logLevel := command.String(flag.LogLevelFlagName)
level, err := zerolog.ParseLevel(logLevel)
if err != nil {
log.Err(flag.ErrInvalidLogLevel{LogLevel: logLevel}).Send()
}
zerolog.SetGlobalLevel(level)
return ctx, nil
}

86
command/healthcheck.go Normal file
View File

@ -0,0 +1,86 @@
package command
import (
"context"
"fmt"
"gitea.illuad.fr/adrien/middleman"
"gitea.illuad.fr/adrien/middleman/flag"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup"
"net/http"
)
// healthcheck is the structure representing the command described below.
type healthcheck struct {
group *errgroup.Group
}
// HealthcheckCmd is the command name.
const HealthcheckCmd = "healthcheck"
const (
// healthcheckURLFlagName is the flag name used to set the healthcheck URL.
healthcheckURLFlagName = "healthcheck-url"
)
const (
// defaultHealthcheckURL is the default healthcheck URL.
defaultHealthcheckURL = "http://127.0.0.1:5732/healthz"
)
var h healthcheck
// Healthcheck describes the healthcheck command.
func Healthcheck(group *errgroup.Group) *cli.Command {
h.group = group
return &cli.Command{
Name: HealthcheckCmd,
Aliases: middleman.PluckAlias(HealthcheckCmd, HealthcheckCmd),
Usage: "Runs healthcheck mode",
Description: "Perform HTTP HEAD request to the health endpoint",
Flags: []cli.Flag{
healthcheckURL(),
flag.LogFormat(HealthcheckCmd),
flag.LogLevel(HealthcheckCmd),
},
Before: before,
Action: h.action,
}
}
// action is executed when the HealthcheckCmd command is called.
func (h healthcheck) action(ctx context.Context, command *cli.Command) error {
if err := ctx.Err(); err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodHead, command.String(healthcheckURLFlagName), http.NoBody)
if err != nil {
return err
}
var res *http.Response
res, err = http.DefaultClient.Do(req)
if err != nil {
return err
}
defer func() {
if err = res.Body.Close(); err != nil {
log.Err(err).Send()
}
}()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("healthcheck failed with the following HTTP code: %d (%s)", res.StatusCode, http.StatusText(res.StatusCode))
}
return nil
}
func healthcheckURL() cli.Flag {
return &cli.StringFlag{
Name: healthcheckURLFlagName,
Category: "monitoring",
Usage: "full healthcheck URL (protocol, address and path)",
Sources: middleman.PluckEnvVar(EnvVarPrefix, healthcheckURLFlagName),
Value: defaultHealthcheckURL,
Aliases: middleman.PluckAlias(HealthcheckCmd, healthcheckURLFlagName),
}
}

466
command/serve.go Normal file
View File

@ -0,0 +1,466 @@
package command
import (
"context"
"errors"
"fmt"
"gitea.illuad.fr/adrien/middleman"
"gitea.illuad.fr/adrien/middleman/flag"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup"
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"regexp"
"strings"
"time"
)
// serve is the structure representing the command described below.
type serve struct {
group *errgroup.Group
}
// addrPorts holds netip.AddrPort of different HTTP servers.
type addrPorts struct {
srvAddrPort, healthAddrPort netip.AddrPort
}
// methodRegex maps HTTP methods (GET, POST, DELETE...) to a compiled regular expression.
type methodRegex = map[string]*regexp.Regexp
// ProxyHandler takes an incoming request and sends it to another server, proxying the response back to the client.
type ProxyHandler struct {
rp *httputil.ReverseProxy
}
// ErrHTTPMethodNotAllowed is returned if the request's HTTP method is not allowed for this container.
type ErrHTTPMethodNotAllowed struct {
httpMethod string
}
// ErrNoMatch is returned if the request's URL path is not allowed for this container.
type ErrNoMatch struct {
path, httpMethod string
}
func (e ErrHTTPMethodNotAllowed) Error() string {
return fmt.Sprintf("%s is not in the list of HTTP methods allowed this container", e.httpMethod)
}
func (e ErrNoMatch) Error() string {
return fmt.Sprintf("%s does not match any registered regular expression for this HTTP method (%s)", e.path, e.httpMethod)
}
// ServeCmd is the command name.
const ServeCmd = "serve"
const (
// listenAddrFlagName is the flag name used to set the listen address and port.
listenAddrFlagName = "listen-addr"
// dockerSocketPathFlagName is the flag name used to set the Docker unix socket path.
dockerSocketPathFlagName = "docker-socket-path"
// shutdownTimeoutFlagName is the flag name used to set the server shutdown timeout.
shutdownTimeoutFlagName = "shutdown-timeout"
// noDockerSocketHealthcheckFlagName is the flag name used to disable the Docker unix socket healthcheck.
noDockerSocketHealthcheckFlagName = "no-docker-socket-healthcheck"
// noShutdownFlagName is the flag name used to disable the application shutdown in case of Docker unix socket healthcheck failure.
noShutdownFlagName = "no-shutdown"
// healthcheckRetryFlagName is the flag name used to set the number of dial attempts with the Docker unix socket before the application shutdown (if enabled).
healthcheckRetryFlagName = "healthcheck-retry"
// healthEndpointFlagName is the flag name used to set the health endpoint.
healthEndpointFlagName = "health-endpoint"
// healthListenAddrFlagName is the flag name used to set the health endpoint listen address and port.
healthListenAddrFlagName = "health-listen-addr"
// addRequestsFlagName is the flag name used to add allowed requests.
addRequestsFlagName = "add-requests"
)
const (
// defaultListenAddr is the proxy default listen address and port.
defaultListenAddr = "0.0.0.0:2375"
// defaultDockerSocketPath is the default Docker socket path.
defaultDockerSocketPath = "/var/run/docker.sock"
// defaultShutdownTimeout is the default server shutdown timeout.
defaultShutdownTimeout = 5 * time.Second
// defaultHealthcheckRetry is the default number of healthcheck retry
defaultHealthcheckRetry uint64 = 3
// defaultHealthEndpoint is the default health endpoint.
defaultHealthEndpoint = "/healthz"
// defaultHealthListenAddr is the proxy health endpoint default listen address and port.
defaultHealthListenAddr = "127.0.0.1:5732"
// defaultAllowedRequest is the proxy default allowed request.
defaultAllowedRequest = "^/(version|containers/.*|events.*)$"
)
var s serve
var (
containerMethodRegex = map[string]methodRegex{
"*": {
http.MethodGet: regexp.MustCompile(defaultAllowedRequest),
},
}
applicatorURLRegex = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]+)\(((?:(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)(?:,(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS))*)?)\):(.*)$`)
validHTTPMethods = map[string]struct{}{
http.MethodGet: {},
http.MethodHead: {},
http.MethodPost: {},
http.MethodPut: {},
http.MethodPatch: {},
http.MethodDelete: {},
http.MethodConnect: {},
http.MethodTrace: {},
http.MethodOptions: {},
}
containerNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]+`)
httpMethodsRegex = regexp.MustCompile(`(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)*`)
)
// Serve describes the serve command.
func Serve(group *errgroup.Group) *cli.Command {
s.group = group
return &cli.Command{
Name: ServeCmd,
Aliases: middleman.PluckAlias(ServeCmd, ServeCmd),
Usage: "Runs serve mode",
Description: "Proxy requests to the Docker socket using defined access control",
Flags: []cli.Flag{
listenAddr(),
dockerSocketPath(),
shutdownTimeout(),
noDockerSocketHealthcheck(),
noShutdown(),
healthcheckRetry(),
healthEndpoint(),
healthListenAddr(),
addRequests(),
flag.LogFormat(ServeCmd),
flag.LogLevel(ServeCmd),
},
Before: before,
Action: s.action,
DisableSliceFlagSeparator: true,
}
}
func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug().Str("http_method", r.Method).Str("path", r.URL.Path).Msg("incoming request")
mr, ok := containerMethodRegex["*"]
if ok {
if err := checkMethodPath(w, r, mr); err != nil {
log.Err(err).Send()
return
}
} else {
var (
containerName string
host, _, _ = net.SplitHostPort(r.RemoteAddr)
ip = net.ParseIP(host)
)
for containerName, mr = range containerMethodRegex {
resolvedIPs, err := net.LookupIP(containerName)
if err != nil {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
for _, resolvedIP := range resolvedIPs {
if resolvedIP.Equal(ip) {
if err = checkMethodPath(w, r, mr); err != nil {
log.Err(err).Send()
return
}
}
}
}
}
ph.rp.ServeHTTP(w, r)
}
// checkMethodPath executes the regular expression on the path of the HTTP request if and only if
// the latter's HTTP method is actually present in the list of authorized HTTP methods.
func checkMethodPath(w http.ResponseWriter, r *http.Request, mr methodRegex) error {
req, ok := mr[r.Method]
if !ok {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return ErrHTTPMethodNotAllowed{httpMethod: r.Method}
}
if !req.MatchString(r.URL.Path) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return ErrNoMatch{
path: r.URL.Path,
httpMethod: r.Method,
}
}
return nil
}
// action is executed when the ServeCmd command is called.
func (s serve) action(ctx context.Context, command *cli.Command) error {
if err := ctx.Err(); err != nil {
return err
}
ap, err := parseAddrPorts(command.String(listenAddrFlagName), command.String(healthListenAddrFlagName))
if err != nil {
return err
}
var l net.Listener
l, err = net.Listen("tcp", ap.srvAddrPort.String())
if err != nil {
return err
}
dummyURL, _ := url.Parse("http://dummy")
rp := httputil.NewSingleHostReverseProxy(dummyURL)
rp.Transport = &http.Transport{
DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
return net.Dial("unix", command.String(dockerSocketPathFlagName))
},
}
srv := &http.Server{ // #nosec: G112
Handler: &ProxyHandler{rp: rp},
}
s.group.Go(func() error {
if err = srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
retry := command.Uint(healthcheckRetryFlagName)
if !command.Bool(noDockerSocketHealthcheckFlagName) {
ticker := time.Tick(2 * time.Second)
s.group.Go(func() error {
loop:
for {
if err = unixDial(command.String(dockerSocketPathFlagName)); err != nil {
if !command.Bool(noShutdownFlagName) {
if retry == 0 {
return err
}
log.Err(err).Uint64("retry_remaining", retry).Send()
retry--
}
}
select {
case <-ticker:
continue
case <-ctx.Done():
break loop
}
}
return ctx.Err()
})
}
mux := http.NewServeMux()
mux.HandleFunc(command.String(healthEndpointFlagName), func(w http.ResponseWriter, r *http.Request) {
log.Trace().Str("from", r.RemoteAddr).Msg("incoming healthcheck request")
if r.Method != http.MethodHead {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if err = unixDial(command.String(dockerSocketPathFlagName)); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
}
})
healthSrv := http.Server{
Addr: ap.healthAddrPort.String(),
Handler: mux,
ReadTimeout: 3 * time.Second,
ReadHeaderTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
}
s.group.Go(func() error {
if err = healthSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
log.Info().
Stringer("listen_addr", ap.srvAddrPort).
Str("docker_socket_path", command.String(dockerSocketPathFlagName)).
Stringer("shutdown_timeout", command.Duration(shutdownTimeoutFlagName)).
Bool("docker_socket_healthcheck_disabled", command.Bool(noDockerSocketHealthcheckFlagName)).
Bool("shutdown_on_failure_disabled", command.Bool(noShutdownFlagName)).
Uint64("number_of_healthcheck_retry", command.Uint(healthcheckRetryFlagName)).
Str("health_endpoint", command.String(healthEndpointFlagName)).
Stringer("health_endpoint_listen_addr", ap.healthAddrPort).
Strs("requests", command.StringSlice(addRequestsFlagName)).
Str("log_format", command.String(flag.LogFormatFlagName)).
Str("log_level", command.String(flag.LogLevelFlagName)).
Msg("middleman started")
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), command.Duration(shutdownTimeoutFlagName))
defer cancel()
if err = healthSrv.Close(); err != nil {
log.Err(err).Send()
}
return srv.Shutdown(shutdownCtx)
}
func parseAddrPorts(srvAddrPort, healthAddrPort string) (addrPorts, error) {
var ap addrPorts
if err := parseAddrPort(srvAddrPort, &ap.srvAddrPort); err != nil {
return ap, err
}
return ap, parseAddrPort(healthAddrPort, &ap.healthAddrPort)
}
func parseAddrPort(addrPortStr string, addrPort *netip.AddrPort) error {
ap, err := netip.ParseAddrPort(addrPortStr)
if err != nil {
return err
}
*addrPort = ap
return nil
}
func unixDial(socketPath string) error {
conn, err := net.DialTimeout("unix", socketPath, 3*time.Second)
if err != nil {
return err
}
return conn.Close()
}
func listenAddr() cli.Flag {
return &cli.StringFlag{
Name: listenAddrFlagName,
Category: "network",
Usage: "proxy listen address",
Sources: middleman.PluckEnvVar(EnvVarPrefix, listenAddrFlagName),
Value: defaultListenAddr,
Aliases: middleman.PluckAlias(ServeCmd, listenAddrFlagName),
}
}
func dockerSocketPath() cli.Flag {
return &cli.StringFlag{
Name: dockerSocketPathFlagName,
Category: "docker",
Usage: "Docker unix socket path",
Sources: middleman.PluckEnvVar(EnvVarPrefix, dockerSocketPathFlagName),
Value: defaultDockerSocketPath,
Aliases: middleman.PluckAlias(ServeCmd, dockerSocketPathFlagName),
}
}
func shutdownTimeout() cli.Flag {
return &cli.DurationFlag{
Name: shutdownTimeoutFlagName,
Category: "network",
Usage: "server shutdown timeout",
Sources: middleman.PluckEnvVar(EnvVarPrefix, shutdownTimeoutFlagName),
Value: defaultShutdownTimeout,
Aliases: middleman.PluckAlias(ServeCmd, shutdownTimeoutFlagName),
}
}
func noDockerSocketHealthcheck() cli.Flag {
return &cli.BoolFlag{
Name: noDockerSocketHealthcheckFlagName,
Category: "docker",
HideDefault: true,
Usage: "disable Docker unix socket healthcheck",
Sources: middleman.PluckEnvVar(EnvVarPrefix, noDockerSocketHealthcheckFlagName),
Aliases: middleman.PluckAlias(ServeCmd, noDockerSocketHealthcheckFlagName),
}
}
func noShutdown() cli.Flag {
return &cli.BoolFlag{
Name: noShutdownFlagName,
Category: "internal monitoring behavior",
HideDefault: true,
Usage: "disable application shutdown in case of Docker unix socket healthcheck failure",
Sources: middleman.PluckEnvVar(EnvVarPrefix, noShutdownFlagName),
Aliases: middleman.PluckAlias(ServeCmd, noShutdownFlagName),
}
}
func healthcheckRetry() cli.Flag {
return &cli.UintFlag{
Name: healthcheckRetryFlagName,
Category: "internal monitoring behavior",
Usage: "number of dial attempts with the Docker unix socket before the application shutdown (if enabled)",
Sources: middleman.PluckEnvVar(EnvVarPrefix, healthcheckRetryFlagName),
Value: defaultHealthcheckRetry,
Aliases: middleman.PluckAlias(ServeCmd, healthcheckRetryFlagName),
}
}
func healthEndpoint() cli.Flag {
return &cli.StringFlag{
Name: healthEndpointFlagName,
Category: "monitoring",
Usage: "health endpoint",
Sources: middleman.PluckEnvVar(EnvVarPrefix, healthEndpointFlagName),
Value: defaultHealthEndpoint,
Aliases: middleman.PluckAlias(ServeCmd, healthEndpointFlagName),
}
}
func healthListenAddr() cli.Flag {
return &cli.StringFlag{
Name: healthListenAddrFlagName,
Category: "monitoring",
Usage: "proxy health endpoint listen address",
Sources: middleman.PluckEnvVar(EnvVarPrefix, healthListenAddrFlagName),
Value: defaultHealthListenAddr,
Aliases: middleman.PluckAlias(ServeCmd, healthListenAddrFlagName),
}
}
func addRequests() cli.Flag {
return &cli.StringSliceFlag{
Name: addRequestsFlagName,
Category: "network",
Usage: "add requests",
Sources: middleman.PluckEnvVar(EnvVarPrefix, addRequestsFlagName),
Local: true, // Required to trigger the Action when this flag is set via the environment variable, see https://github.com/urfave/cli/issues/2041.
Value: []string{"*:" + defaultAllowedRequest},
Aliases: middleman.PluckAlias(ServeCmd, addRequestsFlagName),
Action: func(ctx context.Context, command *cli.Command, requests []string) error {
clear(containerMethodRegex)
for _, request := range requests {
// An applicator is a container name/HTTP method pair e.g., nginx(GET).
// The following regex extracts this applicator and the associated URL regex.
aur := applicatorURLRegex.FindString(request)
if aur == "" {
// If we are here, it means that user set a wildcard (kinda) applicator e.g., GET,POST:URL_REGEX.
// In its extended form, this applicator can be read as follows: *(GET,POST):URL_REGEX.
methods, urlRegex, ok := strings.Cut(request, ":")
if !ok { // || methods == ""?
return errors.New("HTTP method(s) must be specified before ':'")
}
if err := registerMethodRegex("*", urlRegex, strings.Split(methods, ",")); err != nil {
return err
}
}
applicator, urlRegex, _ := strings.Cut(aur, ":")
if err := registerMethodRegex(containerNameRegex.FindString(applicator), urlRegex, httpMethodsRegex.FindAllString(applicator, -1)); err != nil {
return err
}
}
return nil
},
}
}
func registerMethodRegex(containerName, urlRegex string, httpMethods []string) error {
r, err := regexp.Compile(urlRegex)
if err != nil {
return err
}
for _, httpMethod := range httpMethods {
if _, ok := validHTTPMethods[httpMethod]; !ok {
return fmt.Errorf("%s is not a valid HTTP method", httpMethod)
}
if containerMethodRegex[containerName] == nil {
containerMethodRegex[containerName] = make(methodRegex)
}
containerMethodRegex[containerName][httpMethod] = r
}
return nil
}

21
env.go Normal file
View File

@ -0,0 +1,21 @@
package middleman
import (
"github.com/urfave/cli/v3"
"strings"
)
func PluckEnvVar(prefix, flagName string, more ...string) cli.ValueSourceChain {
prefix = strings.ToUpper(strings.TrimSpace(prefix))
key := strings.ToUpper(strings.ReplaceAll(flagName, "-", "_"))
if prefix != "" {
key = prefix + "_" + key
}
n := len(more)
keys := make([]string, 0, n+1)
keys = append(keys, key)
if n > 0 {
keys = append(keys, more...)
}
return cli.EnvVars(keys...)
}

49
flag/flag.go Normal file
View File

@ -0,0 +1,49 @@
package flag
import (
"fmt"
"gitea.illuad.fr/adrien/middleman"
"github.com/rs/zerolog"
"github.com/urfave/cli/v3"
)
const (
LogFormatFlagName = "log-format"
LogLevelFlagName = "log-level"
)
const (
defaultLogFormat = "json"
defaultLogLevel = zerolog.ErrorLevel
)
// ErrInvalidLogLevel is returned if the given log level is invalid.
type ErrInvalidLogLevel struct {
LogLevel string
}
func (e ErrInvalidLogLevel) Error() string {
return fmt.Sprintf("%s: invalid log level, see help", e.LogLevel)
}
func LogFormat(parent string) cli.Flag {
return &cli.StringFlag{
Name: LogFormatFlagName,
Category: "log",
Usage: "log format (hf, json)",
Sources: middleman.PluckEnvVar("MIDDLEMAN", LogFormatFlagName),
Value: defaultLogFormat,
Aliases: middleman.PluckAlias(parent, LogFormatFlagName),
}
}
func LogLevel(parent string) cli.Flag {
return &cli.StringFlag{
Name: LogLevelFlagName,
Category: "log",
Usage: "log level (trace, debug, info, warn, error, fatal, panic)",
Sources: middleman.PluckEnvVar("MIDDLEMAN", LogLevelFlagName),
Value: defaultLogLevel.String(),
Aliases: middleman.PluckAlias(parent, LogLevelFlagName),
}
}

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module gitea.illuad.fr/adrien/middleman
go 1.24.2
require (
github.com/rs/zerolog v1.34.0
github.com/urfave/cli/v3 v3.1.1
golang.org/x/sync v0.13.0
)
require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.32.0 // indirect
)

38
go.sum Normal file
View File

@ -0,0 +1,38 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/urfave/cli/v3 v3.1.1 h1:bNnl8pFI5dxPOjeONvFCDFoECLQsceDG4ejahs4Jtxk=
github.com/urfave/cli/v3 v3.1.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=