diff --git a/.gitignore b/.gitignore index 058cad6..e5df055 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ go.work xmidt-agent internal/jwtxt/cmd/example/* + +*.dot diff --git a/config.go b/config.go index 0623709..735937a 100644 --- a/config.go +++ b/config.go @@ -3,9 +3,79 @@ package main -import "github.com/xmidt-org/sallust" +import ( + "fmt" + "os" + + "github.com/goschtalt/goschtalt" + "github.com/xmidt-org/sallust" + "gopkg.in/dealancer/validate.v2" +) type Config struct { SpecialValue string Logger sallust.Config } + +// Collect and process the configuration files and env vars and +// produce a configuration object. +func provideConfig(cli *CLI) (*goschtalt.Config, error) { + gs, err := goschtalt.New( + goschtalt.StdCfgLayout(applicationName, cli.Files...), + goschtalt.ConfigIs("two_words"), + goschtalt.DefaultUnmarshalOptions( + goschtalt.WithValidator( + goschtalt.ValidatorFunc(validate.Validate), + ), + ), + + // Seed the program with the default, built-in configuration. + // Mark this as a default so it is ordered correctly. + goschtalt.AddValue("built-in", goschtalt.Root, defaultConfig, + goschtalt.AsDefault()), + ) + if err != nil { + return nil, err + } + + if cli.Show { + // handleCLIShow handles the -s/--show option where the configuration is + // shown, then the program is exited. + // + // Exit with success because if the configuration is broken it will be + // very hard to debug where the problem originates. This way you can + // see the configuration and then run the service with the same + // configuration to see the error. + + fmt.Fprintln(os.Stdout, gs.Explain().String()) + + out, err := gs.Marshal() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } else { + fmt.Fprintln(os.Stdout, "## Final Configuration\n---\n"+string(out)) + } + + os.Exit(0) + } + + var tmp Config + err = gs.Unmarshal(goschtalt.Root, &tmp) + if err != nil { + fmt.Fprintln(os.Stderr, "There is a critical error in the configuration.") + fmt.Fprintln(os.Stderr, "Run with -s/--show to see the configuration.") + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + + // Exit here to prevent a very difficult to debug error from occurring. + os.Exit(0) + } + + return gs, nil +} + +// ----------------------------------------------------------------------------- +// Keep the default configuration at the bottom of the file so it is easy to +// see what the default configuration is. +// ----------------------------------------------------------------------------- + +var defaultConfig = Config{} diff --git a/go.mod b/go.mod index ddd4047..1a05bd6 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,14 @@ require ( github.com/xmidt-org/wrp-go/v3 v3.2.0 go.uber.org/fx v1.20.0 go.uber.org/zap v1.26.0 + gopkg.in/dealancer/validate.v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.3.1 // indirect github.com/goschtalt/approx v1.0.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/miekg/dns v1.1.56 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0939a74..5948c85 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= @@ -30,6 +31,9 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= @@ -41,6 +45,13 @@ github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef h1:NKxTG6GVGbfMXc2m github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= @@ -92,7 +103,10 @@ gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/dealancer/validate.v2 v2.1.0 h1:XY95SZhVH1rBe8uwtnQEsOO79rv8GPwK+P3VWhQfJbA= +gopkg.in/dealancer/validate.v2 v2.1.0/go.mod h1:EipWMj8hVO2/dPXVlYRe9yKcgVd5OttpQDiM1/wZ0DE= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/invalid.yml b/invalid.yml new file mode 100644 index 0000000..27a16d8 --- /dev/null +++ b/invalid.yml @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 +--- +invalid: + invalid: invalid diff --git a/main.go b/main.go index b7ec70e..d8cba33 100644 --- a/main.go +++ b/main.go @@ -36,28 +36,28 @@ var ( type CLI struct { Dev bool `optional:"" short:"d" help:"Run in development mode."` Show bool `optional:"" short:"s" help:"Show the configuration and exit."` + Graph string `optional:"" short:"g" help:"Output the dependency graph to the specified file."` Files []string `optional:"" short:"f" help:"Specific configuration files or directories."` } -// xmidiAgent is the main entry point for the program. It is responsible for -// setting up the dependency injection framework and invoking the program. -func xmidtAgent(args []string) error { +// xmidtAgent is the main entry point for the program. It is responsible for +// setting up the dependency injection framework and returning the app object. +func xmidtAgent(args []string) (*fx.App, error) { var ( gscfg *goschtalt.Config - // Capture if the program is being run in dev mode so the extra stuff - // is output as requested. - dev devMode + // Capture the dependency tree in case we need to debug something. + g fx.DotGraph - // Capture if the program should gracefully exit early & without - // reporting an error via logging. - early earlyExit + // Capture the command line arguments. + cli *CLI ) app := fx.New( fx.Supply(cliArgs(args)), - fx.Supply(&early), - fx.Supply(&dev), + fx.Populate(&g), + fx.Populate(&gscfg), + fx.Populate(&cli), fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} @@ -66,54 +66,29 @@ func xmidtAgent(args []string) error { fx.Provide( provideCLI, provideLogger, - - // Collect and process the configuration files and env vars and - // produce a configuration object. - func(cli *CLI) (*goschtalt.Config, error) { - return goschtalt.New( - goschtalt.StdCfgLayout(applicationName, cli.Files...), - goschtalt.ConfigIs("two_words"), - - // Seed the program with the default, built-in configuration - goschtalt.AddValue("built-in", goschtalt.Root, - Config{ - SpecialValue: "default", - }, - goschtalt.AsDefault(), // Mark this as a default so it is ordered correctly - ), - ) - }, + provideConfig, goschtalt.UnmarshalFunc[sallust.Config]("logger", goschtalt.Optional()), ), - fx.Invoke( - handleCLIShow, - func(gs *goschtalt.Config) { - gscfg = gs - }, - ), + fx.Invoke(), ) - if dev { - defer func() { - fmt.Fprintln(os.Stderr, gscfg.Explain().String()) - }() + if cli != nil && cli.Graph != "" { + _ = os.WriteFile(cli.Graph, []byte(g), 0600) } - if err := app.Err(); err != nil || early { - return err + if err := app.Err(); err != nil { + return nil, err } - app.Run() - - return nil + return app, nil } func main() { - err := xmidtAgent(os.Args[1:]) - + app, err := xmidtAgent(os.Args[1:]) if err == nil { + app.Run() return } @@ -121,37 +96,28 @@ func main() { os.Exit(-1) } -// Provides a named type so it's a bit easier to flow through & use in fx. -type earlyExit bool - -// Provides a named type so it's a bit easier to flow through & use in fx. -type devMode bool - // Provides a named type so it's a bit easier to flow through & use in fx. type cliArgs []string -// handleCLIShow handles the -s/--show option where the configuration is shown, -// then the program is exited. -func handleCLIShow(cli *CLI, cfg *goschtalt.Config, early *earlyExit) { - if !cli.Show { - return - } +// Handle the CLI processing and return the processed input. +func provideCLI(args cliArgs) (*CLI, error) { + return provideCLIWithOpts(args, false) +} - fmt.Fprintln(os.Stdout, cfg.Explain().String()) +func provideCLIWithOpts(args cliArgs, testOpts bool) (*CLI, error) { + var cli CLI - out, err := cfg.Marshal() - if err != nil { - fmt.Fprintln(os.Stderr, err) - } else { - fmt.Fprintln(os.Stdout, "## Final Configuration\n---\n"+string(out)) - } + // Create a no-op option to satisfy the kong.New() call. + var opt kong.Option = kong.OptionFunc( + func(*kong.Kong) error { + return nil + }, + ) - *early = earlyExit(true) -} + if testOpts { + opt = kong.Writers(nil, nil) + } -// Handle the CLI processing and return the processed input. -func provideCLI(args cliArgs, dev *devMode, early *earlyExit) (*CLI, error) { - var cli CLI parser, err := kong.New(&cli, kong.Name(applicationName), kong.Description("The cpe agent for Xmidt service.\n"+ @@ -161,25 +127,21 @@ func provideCLI(args cliArgs, dev *devMode, early *earlyExit) (*CLI, error) { fmt.Sprintf("\tBuilt By: %s\n", builtBy), ), kong.UsageOnError(), + opt, ) if err != nil { return nil, err } - parser.Exit = func(i int) { - // Exit early on error, but we still need to return the CLI object - // otherwise fx will complain & hide the useful message we want to print. - *early = earlyExit(true) + if testOpts { + parser.Exit = func(_ int) { panic("exit") } } - fmt.Printf("parser: %p\n", parser) _, err = parser.Parse(args) if err != nil { parser.FatalIfErrorf(err) } - // Mark the devMode state so the collector can be output - *dev = devMode(cli.Dev) return &cli, nil } diff --git a/main_test.go b/main_test.go index b96aa74..b6f5295 100644 --- a/main_test.go +++ b/main_test.go @@ -4,9 +4,10 @@ package main import ( + "context" "testing" + "time" - "github.com/goschtalt/goschtalt" _ "github.com/goschtalt/goschtalt/pkg/typical" _ "github.com/goschtalt/yaml-decoder" _ "github.com/goschtalt/yaml-encoder" @@ -15,49 +16,12 @@ import ( "github.com/xmidt-org/sallust" ) -func Test_handleCLIShow(t *testing.T) { - gs, err := goschtalt.New() - require.NoError(t, err) - require.NotNil(t, gs) - - tests := []struct { - description string - cli *CLI - cfg *goschtalt.Config - expectEarly bool - }{ - { - description: "early exit", - cli: &CLI{ - Show: true, - }, - cfg: gs, - expectEarly: true, - }, { - description: "no early exit", - cli: &CLI{}, - cfg: gs, - }, - } - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - assert := assert.New(t) - - var early earlyExit - handleCLIShow(tc.cli, tc.cfg, &early) - - assert.Equal(tc.expectEarly, bool(early)) - }) - } -} - func Test_provideCLI(t *testing.T) { tests := []struct { description string args cliArgs - earlyExit bool - dev bool want CLI + exits bool expectedErr error }{ { @@ -65,31 +29,36 @@ func Test_provideCLI(t *testing.T) { }, { description: "dev mode", args: cliArgs{"-d"}, - dev: true, want: CLI{Dev: true}, }, { description: "invalid argument", args: cliArgs{"-w"}, - earlyExit: true, + exits: true, }, { description: "invalid argument", args: cliArgs{"-d", "-w"}, - earlyExit: true, + exits: true, + }, { + description: "help", + args: cliArgs{"-h"}, + exits: true, }, } for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { assert := assert.New(t) - var devMode devMode - var earlyExit earlyExit - got, err := provideCLI(tc.args, &devMode, &earlyExit) + if tc.exits { + assert.Panics(func() { + _, _ = provideCLIWithOpts(tc.args, true) + }) + } else { + got, err := provideCLI(tc.args) - assert.ErrorIs(err, tc.expectedErr) - want := tc.want - assert.Equal(&want, got) - assert.Equal(tc.earlyExit, bool(earlyExit)) - assert.Equal(tc.dev, bool(devMode)) + assert.ErrorIs(err, tc.expectedErr) + want := tc.want + assert.Equal(&want, got) + } }) } } @@ -98,24 +67,69 @@ func Test_xmidtAgent(t *testing.T) { tests := []struct { description string args []string + duration time.Duration expectedErr error + panic bool }{ { description: "show config and exit", args: []string{"-s"}, - }, - { + panic: true, + }, { description: "show help and exit", args: []string{"-h"}, + panic: true, + }, { + description: "confirm invalid config file check works", + args: []string{"-f", "invalid.yml"}, + panic: true, + }, { + description: "enable debug mode", + args: []string{"-d"}, + }, { + description: "output graph", + args: []string{"-g", "graph.dot"}, + }, { + description: "start and stop", + duration: time.Millisecond, }, } for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { assert := assert.New(t) + require := require.New(t) + + if tc.panic { + assert.Panics(func() { + _, _ = xmidtAgent(tc.args) + }) + return + } - err := xmidtAgent(tc.args) + app, err := xmidtAgent(tc.args) assert.ErrorIs(err, tc.expectedErr) + if tc.expectedErr != nil { + assert.Nil(app) + return + } + + if tc.duration <= 0 { + return + } + + // only run the program for a few seconds to make sure it starts + startCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err = app.Start(startCtx) + require.NoError(err) + + time.Sleep(tc.duration) + + stopCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err = app.Stop(stopCtx) + require.NoError(err) }) } }