Files
narratio/internal/app/config_commands.go

201 lines
6.6 KiB
Go

package app
import (
"context"
"errors"
"flag"
"fmt"
"io"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
type inspectionFlags struct {
pipelinePath string
campaignPath string
campaignFilePath string
profile singletonStringFlag
}
type inspectionConfig struct {
PipelinePath string
Pipeline *config.PipelineConfig
}
// Config dispatches read-only pipeline configuration commands.
func Config(ctx context.Context, args []string, out io.Writer) error {
if len(args) == 0 {
return fmt.Errorf("config: expected subcommand: validate|show")
}
if args[0] == "--help" || args[0] == "-h" {
writeConfigCommandUsage(out)
return nil
}
switch args[0] {
case "validate":
return ConfigValidate(ctx, args[1:], out)
case "show":
return ConfigShow(ctx, args[1:], out)
default:
return fmt.Errorf("config: unknown subcommand %q", args[0])
}
}
// ConfigValidate validates one fully resolved effective pipeline without
// loading session state or constructing runtime collaborators.
func ConfigValidate(_ context.Context, args []string, out io.Writer) error {
flags, err := parseInspectionFlags("config validate", args, out)
if err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
resolved, err := resolveInspectionConfig(flags)
if err != nil {
return fmt.Errorf("config validate: %w", err)
}
_, err = fmt.Fprintf(out, "Configuration valid: root=%s; profile=%s; digest=%s\n",
inspectionRootPath(resolved), inspectionProfile(resolved.Pipeline), config.EffectivePipelineDigest(resolved.Pipeline))
return err
}
// ConfigShow validates and renders one fully resolved effective pipeline
// without loading session state or constructing runtime collaborators.
func ConfigShow(_ context.Context, args []string, out io.Writer) error {
flags, err := parseInspectionFlags("config show", args, out)
if err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
resolved, err := resolveInspectionConfig(flags)
if err != nil {
return fmt.Errorf("config show: %w", err)
}
data, err := config.MarshalEffectivePipeline(resolved.Pipeline)
if err != nil {
return fmt.Errorf("config show: %w", err)
}
_, err = out.Write(data)
return err
}
func parseInspectionFlags(command string, args []string, out io.Writer) (inspectionFlags, error) {
fs := flag.NewFlagSet(command, flag.ContinueOnError)
fs.SetOutput(out)
var flags inspectionFlags
fs.StringVar(&flags.pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
fs.StringVar(&flags.campaignPath, "campaign", "", "campaign ID")
fs.StringVar(&flags.campaignFilePath, "campaign-file", "", "path to campaign.yml")
flags.profile.name = "profile"
fs.Var(&flags.profile, "profile", "named pipeline profile")
fs.Usage = func() {
fmt.Fprintf(out, "Usage: narratio %s [--config <pipeline.yml>] [--campaign <id> | --campaign-file <campaign.yml>] [--profile <name>]\n\n", command)
fmt.Fprintln(out, "Resolves configuration without a session, workspace, manifest, adapters, or external services.")
fmt.Fprintln(out)
fmt.Fprintln(out, "Flags:")
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
return inspectionFlags{}, fmt.Errorf("%s: invalid flags: %w", command, err)
}
if fs.NArg() != 0 {
return inspectionFlags{}, fmt.Errorf("%s: unexpected positional arguments", command)
}
if strings.TrimSpace(flags.campaignPath) != "" && strings.TrimSpace(flags.campaignFilePath) != "" {
return inspectionFlags{}, fmt.Errorf("%s: --campaign and --campaign-file are mutually exclusive", command)
}
return flags, nil
}
func resolveInspectionConfig(flags inspectionFlags) (*inspectionConfig, error) {
pipelinePath, pipeline, err := loadPipelineConfig(flags.pipelinePath, config.PipelineLoadOptions{Profile: flags.profile.pointer()})
if err != nil {
return nil, err
}
needsCampaign := pipelineHasArtifactFamilies(pipeline)
hasCampaignSelection := strings.TrimSpace(flags.campaignPath) != "" || strings.TrimSpace(flags.campaignFilePath) != ""
if !needsCampaign && !hasCampaignSelection {
if err := validateInspectionPipeline(pipelinePath, pipeline); err != nil {
return nil, err
}
return &inspectionConfig{PipelinePath: pipelinePath, Pipeline: pipeline}, nil
}
campaignPath, err := resolveCampaignConfigPath(pipeline, flags.campaignPath, flags.campaignFilePath)
if err != nil {
if needsCampaign {
return nil, fmt.Errorf("pipeline.scriptorium.artifact_families requires a campaign: %w", err)
}
return nil, err
}
campaign, err := config.LoadCampaign(campaignPath)
if err != nil {
return nil, err
}
if selectedID := strings.TrimSpace(flags.campaignPath); selectedID != "" && strings.TrimSpace(flags.campaignFilePath) == "" {
if got := config.CampaignID(campaign); got != selectedID {
return nil, fmt.Errorf("campaign config %q invalid: campaign_id %q does not match selected campaign %q", campaignPath, got, selectedID)
}
}
loaded, err := config.LoadPipelineCampaign(pipelinePath, pipeline, campaignPath, campaign)
if err != nil {
return nil, err
}
if err := validateInspectionPipeline(loaded.PipelinePath, loaded.Pipeline); err != nil {
return nil, err
}
if err := validateInspectionCampaign(loaded.CampaignPath, loaded.Campaign); err != nil {
return nil, err
}
return &inspectionConfig{
PipelinePath: loaded.PipelinePath,
Pipeline: loaded.Pipeline,
}, nil
}
func pipelineHasArtifactFamilies(pipeline *config.PipelineConfig) bool {
return pipeline != nil && pipeline.Scriptorium != nil && len(pipeline.Scriptorium.ArtifactFamilies) != 0
}
func validateInspectionPipeline(path string, pipeline *config.PipelineConfig) error {
if err := config.ValidatePipelineConfig(pipeline); err != nil {
return fmt.Errorf("pipeline config %q invalid: %w", path, err)
}
return nil
}
func validateInspectionCampaign(path string, campaign *config.CampaignConfig) error {
if err := config.ValidateCampaignConfig(campaign); err != nil {
return fmt.Errorf("campaign config %q invalid: %w", path, err)
}
return nil
}
func inspectionRootPath(resolved *inspectionConfig) string {
if resolved == nil {
return ""
}
if root := config.EffectivePipelineRootPath(resolved.Pipeline); root != "" {
return root
}
return resolved.PipelinePath
}
func inspectionProfile(pipeline *config.PipelineConfig) string {
if profile, ok := config.SelectedPipelineProfile(pipeline); ok {
return profile.Name
}
return "none"
}
func writeConfigCommandUsage(out io.Writer) {
fmt.Fprintln(out, "Usage: narratio config <validate|show>")
fmt.Fprintln(out)
fmt.Fprintln(out, "Use config validate to check an effective pipeline or config show to print normalized effective YAML.")
}