271 lines
6.9 KiB
Go
271 lines
6.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
const defaultConfigPath = "/usr/local/etc/notarius/config.yml"
|
|
|
|
const usage = `Usage:
|
|
notarius help
|
|
notarius config validate --config path/to/config.yml [--pipeline pipeline-id] [--only lane-a,lane-b]
|
|
notarius pipelines list --config path/to/config.yml [--json]
|
|
`
|
|
|
|
type Options struct {
|
|
Catalog pipeline.ModuleCatalog
|
|
LookupEnv func(string) (string, bool)
|
|
}
|
|
|
|
// Run executes the command-line interface and returns a process exit code.
|
|
func Run(args []string, stdout, stderr io.Writer) int {
|
|
return RunWithOptions(args, stdout, stderr, Options{})
|
|
}
|
|
|
|
func RunWithOptions(args []string, stdout, stderr io.Writer, opts Options) int {
|
|
opts = normalizeOptions(opts)
|
|
if len(args) == 0 {
|
|
writeUsage(stdout)
|
|
return 0
|
|
}
|
|
|
|
switch args[0] {
|
|
case "help", "--help", "-h":
|
|
writeUsage(stdout)
|
|
return 0
|
|
case "config":
|
|
return runConfig(args[1:], stdout, stderr, opts)
|
|
case "pipelines":
|
|
return runPipelines(args[1:], stdout, stderr, opts)
|
|
default:
|
|
fmt.Fprintf(stderr, "notarius: unknown command %q\n", args[0])
|
|
writeUsage(stderr)
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func writeUsage(w io.Writer) {
|
|
fmt.Fprint(w, usage)
|
|
}
|
|
|
|
func normalizeOptions(opts Options) Options {
|
|
if opts.LookupEnv == nil {
|
|
opts.LookupEnv = os.LookupEnv
|
|
}
|
|
return opts
|
|
}
|
|
|
|
func runConfig(args []string, stdout, stderr io.Writer, opts Options) int {
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(stderr, "notarius: config requires a subcommand")
|
|
writeUsage(stderr)
|
|
return 2
|
|
}
|
|
switch args[0] {
|
|
case "validate":
|
|
return runConfigValidate(args[1:], stdout, stderr, opts)
|
|
default:
|
|
fmt.Fprintf(stderr, "notarius: unknown config subcommand %q\n", args[0])
|
|
writeUsage(stderr)
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func runConfigValidate(args []string, stdout, stderr io.Writer, opts Options) int {
|
|
fs := flag.NewFlagSet("config validate", flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
configPath := fs.String("config", "", "config file path")
|
|
pipelineID := fs.String("pipeline", "", "pipeline ID")
|
|
onlyRaw := fs.String("only", "", "comma-separated artifact lanes")
|
|
if err := fs.Parse(args); err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
|
|
return 2
|
|
}
|
|
if strings.TrimSpace(*onlyRaw) != "" && strings.TrimSpace(*pipelineID) == "" {
|
|
fmt.Fprintln(stderr, "notarius: --only requires --pipeline")
|
|
return 2
|
|
}
|
|
only, err := parseOnly(*onlyRaw)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 2
|
|
}
|
|
|
|
cfg, path, err := loadConfig(*configPath, opts)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
if strings.TrimSpace(*pipelineID) != "" {
|
|
if _, err := cfg.Resolve(config.ResolveInput{
|
|
PipelineID: *pipelineID,
|
|
Only: only,
|
|
Catalog: opts.Catalog,
|
|
}); err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Fprintf(stdout, "config %q is valid for pipeline %q\n", path, strings.TrimSpace(*pipelineID))
|
|
return 0
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Fprintf(stdout, "config %q is valid\n", path)
|
|
return 0
|
|
}
|
|
|
|
func runPipelines(args []string, stdout, stderr io.Writer, opts Options) int {
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(stderr, "notarius: pipelines requires a subcommand")
|
|
writeUsage(stderr)
|
|
return 2
|
|
}
|
|
switch args[0] {
|
|
case "list":
|
|
return runPipelinesList(args[1:], stdout, stderr, opts)
|
|
default:
|
|
fmt.Fprintf(stderr, "notarius: unknown pipelines subcommand %q\n", args[0])
|
|
writeUsage(stderr)
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func runPipelinesList(args []string, stdout, stderr io.Writer, opts Options) int {
|
|
fs := flag.NewFlagSet("pipelines list", flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
configPath := fs.String("config", "", "config file path")
|
|
jsonOutput := fs.Bool("json", false, "write JSON output")
|
|
if err := fs.Parse(args); err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
|
|
return 2
|
|
}
|
|
|
|
cfg, _, err := loadConfig(*configPath, opts)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 1
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
ids := sortedPipelineIDs(cfg)
|
|
if *jsonOutput {
|
|
payload := struct {
|
|
Pipelines []string `json:"pipelines"`
|
|
}{Pipelines: ids}
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "notarius: marshal pipeline list: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Fprintf(stdout, "%s\n", encoded)
|
|
return 0
|
|
}
|
|
|
|
for _, id := range ids {
|
|
fmt.Fprintln(stdout, id)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func loadConfig(configPath string, opts Options) (config.Config, string, error) {
|
|
path, err := discoverConfigPath(configPath, opts)
|
|
if err != nil {
|
|
return config.Config{}, "", err
|
|
}
|
|
|
|
fileCfg, err := config.LoadFileConfig(path)
|
|
if err != nil {
|
|
return config.Config{}, "", err
|
|
}
|
|
cfg := config.Default()
|
|
if err := cfg.ApplyFileConfigWithLookup(fileCfg, opts.LookupEnv); err != nil {
|
|
return config.Config{}, "", err
|
|
}
|
|
if err := cfg.ApplyEnvOverridesWithLookup(opts.LookupEnv); err != nil {
|
|
return config.Config{}, "", err
|
|
}
|
|
return cfg, path, nil
|
|
}
|
|
|
|
func discoverConfigPath(configPath string, opts Options) (string, error) {
|
|
if path := strings.TrimSpace(configPath); path != "" {
|
|
if err := requireConfigFile(path); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
if path, ok := opts.LookupEnv("NOTARIUS_CONFIG"); ok && strings.TrimSpace(path) != "" {
|
|
path = strings.TrimSpace(path)
|
|
if err := requireConfigFile(path); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
if _, err := os.Stat(defaultConfigPath); err == nil {
|
|
return defaultConfigPath, nil
|
|
} else if err != nil && !os.IsNotExist(err) {
|
|
return "", fmt.Errorf("check default config %q: %w", defaultConfigPath, err)
|
|
}
|
|
return "", fmt.Errorf("config file not found; pass --config or set NOTARIUS_CONFIG")
|
|
}
|
|
|
|
func requireConfigFile(path string) error {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("config file %q is not available: %w", path, err)
|
|
}
|
|
if info.IsDir() {
|
|
return fmt.Errorf("config file %q is a directory", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseOnly(raw string) ([]string, error) {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return nil, nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
trimmed := strings.TrimSpace(part)
|
|
if trimmed == "" {
|
|
return nil, fmt.Errorf("--only must contain comma-separated non-empty artifact lane IDs")
|
|
}
|
|
result = append(result, trimmed)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func sortedPipelineIDs(cfg config.Config) []string {
|
|
ids := make([]string, 0, len(cfg.Pipelines))
|
|
for id := range cfg.Pipelines {
|
|
ids = append(ids, strings.TrimSpace(id))
|
|
}
|
|
sort.Strings(ids)
|
|
return ids
|
|
}
|