diff --git a/cmd/gateway/gateway_suite_test.go b/cmd/gateway/gateway_suite_test.go new file mode 100644 index 0000000000..878d92fbdf --- /dev/null +++ b/cmd/gateway/gateway_suite_test.go @@ -0,0 +1,13 @@ +package main_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGateway(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gateway Suite") +} diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 39b182bd07..39e6afe9a5 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "github.com/nginxinc/nginx-gateway-kubernetes/internal/config" @@ -10,6 +11,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" ) +const ( + domain string = "gateway.nginx.org" +) + var ( // Set during go build version string @@ -17,23 +22,27 @@ var ( date string // Command-line flags - gatewayCtlrName = flag.String("gateway-ctlr-name", "", "The name of the Gateway controller") + gatewayCtlrName = flag.String( + "gateway-ctlr-name", + "", + fmt.Sprintf("The name of the Gateway controller. The controller name must be of the form: DOMAIN/NAMESPACE/NAME. The controller's domain is '%s'.", domain), + ) ) func main() { flag.Parse() - if *gatewayCtlrName == "" { - flag.PrintDefaults() - os.Exit(1) - } - logger := zap.New() conf := config.Config{ GatewayCtlrName: *gatewayCtlrName, Logger: logger, } + MustValidateArguments( + flag.CommandLine, + GatewayControllerParam(domain, "nginx-gateway" /* TODO dynamically set */), + ) + logger.Info("Starting NGINX Gateway", "version", version, "commit", commit, diff --git a/cmd/gateway/setup.go b/cmd/gateway/setup.go new file mode 100644 index 0000000000..9961153cfb --- /dev/null +++ b/cmd/gateway/setup.go @@ -0,0 +1,95 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + flag "github.com/spf13/pflag" +) + +const ( + errTmpl = "failed validation - flag: '--%s' reason: '%s'\n" +) + +type Validator func(*flag.FlagSet) error +type ValidatorContext struct { + Key string + V Validator +} + +func GatewayControllerParam(domain string, namespace string) ValidatorContext { + name := "gateway-ctlr-name" + return ValidatorContext{ + name, + func(flagset *flag.FlagSet) error { + // FIXME(yacobucci) this does not provide the same regex validation as + // GatewayClass.ControllerName. provide equal and then specific validation + param, err := flagset.GetString(name) + if err != nil { + return err + } + + if len(param) == 0 { + return errors.New("flag must be set") + } + + fields := strings.Split(param, "/") + l := len(fields) + if l != 3 { + return errors.New("unsupported path length, must be form DOMAIN/NAMESPACE/NAME") + } + + for i := len(fields); i > 0; i-- { + switch i { + case 3: + if fields[0] != domain { + return fmt.Errorf("invalid domain: %s", fields[0]) + } + fields = fields[1:] + case 2: + if fields[0] != namespace { + return fmt.Errorf("cross namespace unsupported: %s", fields[0]) + } + fields = fields[1:] + case 1: + if fields[0] == "" { + return errors.New("must provide a name") + } + } + } + + return nil + }, + } +} + +func ValidateArguments(flagset *flag.FlagSet, validators ...ValidatorContext) []string { + var msgs []string + for _, v := range validators { + if flagset.Lookup(v.Key) != nil { + err := v.V(flagset) + if err != nil { + msgs = append(msgs, fmt.Sprintf(errTmpl, v.Key, err.Error())) + } + } + } + + return msgs +} + +func MustValidateArguments(flagset *flag.FlagSet, validators ...ValidatorContext) { + msgs := ValidateArguments(flagset, validators...) + if msgs != nil { + for i := range msgs { + fmt.Fprintf(os.Stderr, "%s", msgs[i]) + } + fmt.Fprintln(os.Stderr, "") + + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + + os.Exit(1) + } +} diff --git a/cmd/gateway/setup_test.go b/cmd/gateway/setup_test.go new file mode 100644 index 0000000000..51e050d894 --- /dev/null +++ b/cmd/gateway/setup_test.go @@ -0,0 +1,237 @@ +package main_test + +import ( + "errors" + + . "github.com/nginxinc/nginx-gateway-kubernetes/cmd/gateway" + flag "github.com/spf13/pflag" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var domain string + +func MockValidator(name string, called *int, succeed bool) ValidatorContext { + return ValidatorContext{ + name, + func(_ *flag.FlagSet) error { + *called++ + + if !succeed { + return errors.New("Mock error") + } + return nil + }, + } +} + +var _ = Describe("Main", func() { + Describe("Generic Validator", func() { + var mockFlags *flag.FlagSet + BeforeEach(func() { + mockFlags = flag.NewFlagSet("mock", flag.PanicOnError) + _ = mockFlags.String("validator-1", "", "validator-1") + _ = mockFlags.String("validator-2", "", "validator-2") + _ = mockFlags.String("validator-3", "", "validator-3") + err := mockFlags.Parse([]string{}) + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + mockFlags = nil + }) + It("should call all validators", func() { + var called int + table := []struct { + ExpectedCalls int + Success bool + Contexts []ValidatorContext + }{ + { + 0, + true, + []ValidatorContext{}, + }, + { + 0, + true, + []ValidatorContext{ + MockValidator("no-flag-set", &called, true), + }, + }, + { + 1, + true, + []ValidatorContext{ + MockValidator("validator-1", &called, true), + }, + }, + { + 1, + true, + []ValidatorContext{ + MockValidator("no-flag-set", &called, true), + MockValidator("validator-1", &called, true), + }, + }, + { + 2, + true, + []ValidatorContext{ + MockValidator("validator-1", &called, true), + MockValidator("validator-2", &called, true), + }, + }, + { + 3, + true, + []ValidatorContext{ + MockValidator("validator-1", &called, true), + MockValidator("validator-2", &called, true), + MockValidator("validator-3", &called, true), + }, + }, + { + 3, + false, + []ValidatorContext{ + MockValidator("validator-1", &called, false), + MockValidator("validator-2", &called, true), + MockValidator("validator-3", &called, true), + }, + }, + { + 3, + false, + []ValidatorContext{ + MockValidator("validator-1", &called, true), + MockValidator("validator-2", &called, true), + MockValidator("validator-3", &called, false), + }, + }, + } + + for i := range table { + called = 0 + msgs := ValidateArguments(mockFlags, table[i].Contexts...) + Expect(msgs == nil).To(Equal(table[i].Success)) + Expect(called).To(Equal(table[i].ExpectedCalls)) + } + }) // should call all validators + }) // Generic Validator + + Describe("CLI argument validation", func() { + type testCase struct { + Param string + Domain string + ExpError bool + } + + var mockFlags *flag.FlagSet + var gatewayCtlrName string + + tester := func(t testCase) { + err := mockFlags.Set(gatewayCtlrName, t.Param) + Expect(err).ToNot(HaveOccurred()) + + v := GatewayControllerParam(domain, t.Domain) + Expect(v.V).ToNot(BeNil()) + + err = v.V(mockFlags) + if t.ExpError { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).ToNot(HaveOccurred()) + } + } + runner := func(table []testCase) { + for i := range table { + tester(table[i]) + } + } + + BeforeEach(func() { + domain = "k8s-gateway.nginx.org" + gatewayCtlrName = "gateway-ctlr-name" + + mockFlags = flag.NewFlagSet("mock", flag.PanicOnError) + _ = mockFlags.String("gateway-ctlr-name", "", "mock gateway-ctlr-name") + err := mockFlags.Parse([]string{}) + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + mockFlags = nil + }) + It("should parse full gateway-ctlr-name", func() { + t := testCase{ + "k8s-gateway.nginx.org/nginx-gateway/my-gateway", + "nginx-gateway", + false, + } + tester(t) + }) // should parse full gateway-ctlr-name + + It("should fail with too many path elements", func() { + t := testCase{ + "k8s-gateway.nginx.org/nginx-gateway/my-gateway/broken", + "nginx-gateway", + true, + } + tester(t) + }) // should fail with too many path elements + + It("should fail with too few path elements", func() { + table := []testCase{ + { + Param: "nginx-gateway/my-gateway", + Domain: "nginx-gateway", + ExpError: true, + }, + { + Param: "my-gateway", + Domain: "nginx-gateway", + ExpError: true, + }, + } + + runner(table) + }) // should fail with too few path elements + + It("should verify constraints", func() { + table := []testCase{ + { + // bad domain + Param: "invalid-domain/nginx-gateway/my-gateway", + Domain: "nginx-gateway", + ExpError: true, + }, + { + // bad domain + Param: "/default/my-gateway", + Domain: "nginx-gateway", + ExpError: true, + }, + { + // bad namespace + Param: "k8s-gateway.nginx.org/default/my-gateway", + Domain: "nginx-gateway", + ExpError: true, + }, + { + // bad namespace + Param: "k8s-gateway.nginx.org//my-gateway", + Domain: "nginx-gateway", + ExpError: true, + }, + { + // bad name + Param: "k8s-gateway.nginx.org/default/", + Domain: "nginx-gateway", + ExpError: true, + }, + } + + runner(table) + }) // should verify constraints + }) // CLI argument validation +}) // end Main diff --git a/go.mod b/go.mod index 6207e993d6..50f83bc1dc 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.1.2 // indirect github.com/googleapis/gnostic v0.5.5 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,6 +56,7 @@ require ( github.com/prometheus/common v0.28.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/spf13/cobra v1.2.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.19.1 // indirect diff --git a/go.sum b/go.sum index c947bf52a6..8df48e3c56 100644 --- a/go.sum +++ b/go.sum @@ -247,6 +247,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -287,6 +288,7 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -294,10 +296,12 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=