Add initial distributor CLI skeleton
This commit is contained in:
10
README.md
10
README.md
@@ -1,5 +1,11 @@
|
|||||||
# distributor
|
# distributor
|
||||||
|
|
||||||
`distributor` is a planned Go application for validating and publishing manifested Markdown bundles.
|
`distributor` is a Go application shell for validating and publishing manifested report bundles.
|
||||||
|
|
||||||
Implementation has not started yet. Current design and implementation planning lives under `docs/roadmap/`.
|
Current implemented behavior is limited to CLI help, version output, and placeholder operational commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run ./cmd/distributor --help
|
||||||
|
```
|
||||||
|
|
||||||
|
See [docs/cli.md](docs/cli.md) for the implemented CLI surface. Current design and implementation planning lives under `docs/roadmap/`.
|
||||||
|
|||||||
12
cmd/distributor/main.go
Normal file
12
cmd/distributor/main.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
os.Exit(cli.Execute(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
|
||||||
|
}
|
||||||
33
docs/cli.md
Normal file
33
docs/cli.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Distributor CLI
|
||||||
|
|
||||||
|
## Shortest useful command
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run ./cmd/distributor --help
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints the currently implemented command shell.
|
||||||
|
|
||||||
|
## Command overview
|
||||||
|
|
||||||
|
```sh
|
||||||
|
distributor --help
|
||||||
|
distributor version
|
||||||
|
distributor run
|
||||||
|
distributor validate
|
||||||
|
distributor inspect
|
||||||
|
```
|
||||||
|
|
||||||
|
`version` prints the application name and version. The default development version is `dev`; release builds may replace it at build time.
|
||||||
|
|
||||||
|
`run`, `validate`, and `inspect` are command placeholders. They intentionally fail with a clear `not implemented` error until the corresponding application behavior exists.
|
||||||
|
|
||||||
|
## Flag reference
|
||||||
|
|
||||||
|
The root command supports:
|
||||||
|
|
||||||
|
- `--help`, `-h`: print root help.
|
||||||
|
|
||||||
|
Each implemented subcommand supports:
|
||||||
|
|
||||||
|
- `--help`, `-h`: print command-specific help.
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module gitea.maximumdirect.net/eric/distributor
|
||||||
|
|
||||||
|
go 1.26
|
||||||
14
internal/app/app.go
Normal file
14
internal/app/app.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
const Name = "distributor"
|
||||||
|
|
||||||
|
// Version can be replaced at build time with -ldflags "-X .../internal/app.Version=<value>".
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
var ErrNotImplemented = errors.New("not implemented")
|
||||||
|
|
||||||
|
func VersionString() string {
|
||||||
|
return Name + " " + Version
|
||||||
|
}
|
||||||
16
internal/app/app_test.go
Normal file
16
internal/app/app_test.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestVersionString(t *testing.T) {
|
||||||
|
oldVersion := Version
|
||||||
|
t.Cleanup(func() {
|
||||||
|
Version = oldVersion
|
||||||
|
})
|
||||||
|
|
||||||
|
Version = "1.2.3"
|
||||||
|
|
||||||
|
if got, want := VersionString(), "distributor 1.2.3"; got != want {
|
||||||
|
t.Fatalf("VersionString() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
internal/app/inspect.go
Normal file
14
internal/app/inspect.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InspectOptions struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Inspect(context.Context, InspectOptions) error {
|
||||||
|
return fmt.Errorf("inspect command: %w", ErrNotImplemented)
|
||||||
|
}
|
||||||
5
internal/app/pipeline.go
Normal file
5
internal/app/pipeline.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
type Pipeline struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
12
internal/app/run.go
Normal file
12
internal/app/run.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RunOptions struct{}
|
||||||
|
|
||||||
|
func Run(context.Context, RunOptions) error {
|
||||||
|
return fmt.Errorf("run command: %w", ErrNotImplemented)
|
||||||
|
}
|
||||||
14
internal/app/validate.go
Normal file
14
internal/app/validate.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValidateOptions struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(context.Context, ValidateOptions) error {
|
||||||
|
return fmt.Errorf("validate command: %w", ErrNotImplemented)
|
||||||
|
}
|
||||||
36
internal/cli/inspect.go
Normal file
36
internal/cli/inspect.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func inspectCommand(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||||
|
if hasHelp(args) {
|
||||||
|
printInspectHelp(stdout)
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
fmt.Fprintf(stderr, "%s: inspect accepts at most one path\n", app.Name)
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
var path string
|
||||||
|
if len(args) == 1 {
|
||||||
|
path = args[0]
|
||||||
|
}
|
||||||
|
if err := app.Inspect(ctx, app.InspectOptions{Path: path}); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func printInspectHelp(w io.Writer) {
|
||||||
|
fmt.Fprint(w, `Usage:
|
||||||
|
distributor inspect <path>
|
||||||
|
|
||||||
|
The inspect command is present but bundle inspection is not implemented yet.
|
||||||
|
`)
|
||||||
|
}
|
||||||
84
internal/cli/root.go
Normal file
84
internal/cli/root.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
exitOK = 0
|
||||||
|
exitError = 1
|
||||||
|
exitUsage = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func Execute(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||||
|
if len(args) == 0 {
|
||||||
|
printRootHelp(stdout)
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "-h", "--help", "help":
|
||||||
|
printRootHelp(stdout)
|
||||||
|
return exitOK
|
||||||
|
case "version":
|
||||||
|
return versionCommand(ctx, args[1:], stdout, stderr)
|
||||||
|
case "run":
|
||||||
|
return runCommand(ctx, args[1:], stdout, stderr)
|
||||||
|
case "validate":
|
||||||
|
return validateCommand(ctx, args[1:], stdout, stderr)
|
||||||
|
case "inspect":
|
||||||
|
return inspectCommand(ctx, args[1:], stdout, stderr)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(stderr, "%s: unknown command %q\n\n", app.Name, args[0])
|
||||||
|
printRootHelp(stderr)
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRootHelp(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `%s validates and publishes manifested report bundles.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
%s <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
version Print version information
|
||||||
|
run Run configured distribution pipelines
|
||||||
|
validate Validate a source bundle or bundle tree
|
||||||
|
inspect Inspect bundles or distributor state
|
||||||
|
|
||||||
|
Use "%s <command> --help" for command-specific help.
|
||||||
|
`, app.Name, app.Name, app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasHelp(args []string) bool {
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-h" || arg == "--help" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(stderr io.Writer, err error) int {
|
||||||
|
if errors.Is(err, app.ErrNotImplemented) {
|
||||||
|
fmt.Fprintf(stderr, "%s: %s\n", app.Name, err)
|
||||||
|
return exitError
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stderr, "%s: %s\n", app.Name, err)
|
||||||
|
return exitError
|
||||||
|
}
|
||||||
|
|
||||||
|
func rejectExtraArgs(stderr io.Writer, command string, args []string) bool {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stderr, "%s: %s does not accept arguments: %s\n", app.Name, command, strings.Join(args, " "))
|
||||||
|
return true
|
||||||
|
}
|
||||||
75
internal/cli/root_test.go
Normal file
75
internal/cli/root_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecuteRootHelp(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := Execute(context.Background(), []string{"--help"}, &stdout, &stderr)
|
||||||
|
|
||||||
|
if code != exitOK {
|
||||||
|
t.Fatalf("exit code = %d, want %d", code, exitOK)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), "Usage:") {
|
||||||
|
t.Fatalf("stdout = %q, want help text", stdout.String())
|
||||||
|
}
|
||||||
|
if stderr.Len() != 0 {
|
||||||
|
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteVersion(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := Execute(context.Background(), []string{"version"}, &stdout, &stderr)
|
||||||
|
|
||||||
|
if code != exitOK {
|
||||||
|
t.Fatalf("exit code = %d, want %d", code, exitOK)
|
||||||
|
}
|
||||||
|
if got, want := stdout.String(), "distributor dev\n"; got != want {
|
||||||
|
t.Fatalf("stdout = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if stderr.Len() != 0 {
|
||||||
|
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaceholderCommandsFailClearly(t *testing.T) {
|
||||||
|
tests := []string{"run", "validate", "inspect"}
|
||||||
|
|
||||||
|
for _, command := range tests {
|
||||||
|
t.Run(command, func(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := Execute(context.Background(), []string{command}, &stdout, &stderr)
|
||||||
|
|
||||||
|
if code != exitError {
|
||||||
|
t.Fatalf("exit code = %d, want %d", code, exitError)
|
||||||
|
}
|
||||||
|
if stdout.Len() != 0 {
|
||||||
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "not implemented") {
|
||||||
|
t.Fatalf("stderr = %q, want not implemented error", stderr.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownCommandIsUsageError(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := Execute(context.Background(), []string{"nope"}, &stdout, &stderr)
|
||||||
|
|
||||||
|
if code != exitUsage {
|
||||||
|
t.Fatalf("exit code = %d, want %d", code, exitUsage)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "unknown command") {
|
||||||
|
t.Fatalf("stderr = %q, want unknown command error", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
32
internal/cli/run.go
Normal file
32
internal/cli/run.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCommand(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||||
|
if hasHelp(args) {
|
||||||
|
printRunHelp(stdout)
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
if len(args) > 0 {
|
||||||
|
fmt.Fprintf(stderr, "%s: run does not accept options yet: %v\n", app.Name, args)
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
if err := app.Run(ctx, app.RunOptions{}); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRunHelp(w io.Writer) {
|
||||||
|
fmt.Fprint(w, `Usage:
|
||||||
|
distributor run
|
||||||
|
|
||||||
|
The run command is present but distribution behavior is not implemented yet.
|
||||||
|
`)
|
||||||
|
}
|
||||||
36
internal/cli/validate.go
Normal file
36
internal/cli/validate.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateCommand(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||||
|
if hasHelp(args) {
|
||||||
|
printValidateHelp(stdout)
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
fmt.Fprintf(stderr, "%s: validate accepts at most one path\n", app.Name)
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
var path string
|
||||||
|
if len(args) == 1 {
|
||||||
|
path = args[0]
|
||||||
|
}
|
||||||
|
if err := app.Validate(ctx, app.ValidateOptions{Path: path}); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func printValidateHelp(w io.Writer) {
|
||||||
|
fmt.Fprint(w, `Usage:
|
||||||
|
distributor validate <path>
|
||||||
|
|
||||||
|
The validate command is present but bundle validation is not implemented yet.
|
||||||
|
`)
|
||||||
|
}
|
||||||
29
internal/cli/version.go
Normal file
29
internal/cli/version.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func versionCommand(_ context.Context, args []string, stdout, stderr io.Writer) int {
|
||||||
|
if hasHelp(args) {
|
||||||
|
printVersionHelp(stdout)
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
if rejectExtraArgs(stderr, "version", args) {
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
fmt.Fprintln(stdout, app.VersionString())
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersionHelp(w io.Writer) {
|
||||||
|
fmt.Fprint(w, `Usage:
|
||||||
|
distributor version
|
||||||
|
|
||||||
|
Print version information.
|
||||||
|
`)
|
||||||
|
}
|
||||||
3
internal/config/config.go
Normal file
3
internal/config/config.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Config struct{}
|
||||||
3
internal/config/defaults.go
Normal file
3
internal/config/defaults.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
const DefaultConfigPath = "/usr/local/etc/distributor/config.yml"
|
||||||
7
internal/logging/logging.go
Normal file
7
internal/logging/logging.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
func Configure(io.Writer) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user