Skip to content

Commit

Permalink
Support multiple commands in gateway binary (#656)
Browse files Browse the repository at this point in the history
This commit:
- Changes the cmd package to support multiple commands in
gateway binary.
- Adds the root command, which simply prints help.
- Adds static-mode command, which starts 
NGINX Kubernetes Gateway in static mode --
the previously available functionally of the gateway binary.
- Adds provisioner-mode command, currently not implemented.

Needed by #634
  • Loading branch information
pleshakov authored May 20, 2023
1 parent 82d32fa commit dc046cc
Show file tree
Hide file tree
Showing 14 changed files with 509 additions and 512 deletions.
135 changes: 135 additions & 0 deletions cmd/gateway/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package main

import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/nginxinc/nginx-kubernetes-gateway/internal/config"
"github.com/nginxinc/nginx-kubernetes-gateway/internal/manager"
)

const (
domain = "k8s-gateway.nginx.org"
gatewayClassFlag = "gatewayclass"
gatewayClassNameUsage = `The name of the GatewayClass resource. ` +
`Every NGINX Gateway must have a unique corresponding GatewayClass resource.`
gatewayCtrlNameFlag = "gateway-ctlr-name"
gatewayCtrlNameUsageFmt = `The name of the Gateway controller. ` +
`The controller name must be of the form: DOMAIN/PATH. The controller's domain is '%s'`
)

var (
// Backing values for common cli flags shared among all subcommands
// The values are managed by the Root command.
gatewayCtlrName = stringValidatingValue{
validator: validateGatewayControllerName,
}

gatewayClassName = stringValidatingValue{
validator: validateResourceName,
}
)

// stringValidatingValue is a string flag value with custom validation logic.
// stringValidatingValue implements the pflag.Value interface.
type stringValidatingValue struct {
validator func(v string) error
value string
}

func (v *stringValidatingValue) String() string {
return v.value
}

func (v *stringValidatingValue) Set(param string) error {
if err := v.validator(param); err != nil {
return err
}
v.value = param
return nil
}

func (v *stringValidatingValue) Type() string {
return "string"
}

func createRootCommand() *cobra.Command {
rootCmd := &cobra.Command{
Use: "gateway",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}

rootCmd.PersistentFlags().Var(
&gatewayCtlrName,
gatewayCtrlNameFlag,
fmt.Sprintf(gatewayCtrlNameUsageFmt, domain),
)
utilruntime.Must(rootCmd.MarkPersistentFlagRequired(gatewayCtrlNameFlag))

rootCmd.PersistentFlags().Var(
&gatewayClassName,
gatewayClassFlag,
gatewayClassNameUsage,
)
utilruntime.Must(rootCmd.MarkPersistentFlagRequired(gatewayClassFlag))

return rootCmd
}

func createStaticModeCommand() *cobra.Command {
return &cobra.Command{
Use: "static-mode",
Short: "Configure NGINX in the scope of a single Gateway resource",
RunE: func(cmd *cobra.Command, args []string) error {
logger := zap.New()
logger.Info("Starting NGINX Kubernetes Gateway in static mode",
"version", version,
"commit", commit,
"date", date,
)

podIP := os.Getenv("POD_IP")
if err := validateIP(podIP); err != nil {
return fmt.Errorf("error validating POD_IP environment variable: %w", err)
}

conf := config.Config{
GatewayCtlrName: gatewayCtlrName.value,
Logger: logger,
GatewayClassName: gatewayClassName.value,
PodIP: podIP,
}

if err := manager.Start(conf); err != nil {
return fmt.Errorf("failed to start control loop: %w", err)
}

return nil
},
}
}

func createProvisionerModeCommand() *cobra.Command {
return &cobra.Command{
Use: "provisioner-mode",
Short: "Provision a static-mode NGINX Gateway Deployment per Gateway resource",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
logger := zap.New()
logger.Info("Starting NGINX Kubernetes Gateway Provisioner",
"version", version,
"commit", commit,
"date", date,
)

return errors.New("not implemented yet")
},
}
}
100 changes: 100 additions & 0 deletions cmd/gateway/commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"io"
"testing"

. "github.com/onsi/gomega"
)

func TestRootCmdFlagValidation(t *testing.T) {
tests := []struct {
name string
expectedErrPrefix string
args []string
wantErr bool
}{
{
name: "valid flags",
args: []string{
"--gateway-ctlr-name=k8s-gateway.nginx.org/nginx-gateway",
"--gatewayclass=nginx",
},
wantErr: false,
},
{
name: "gateway-ctlr-name is not set",
args: []string{
"--gatewayclass=nginx",
},
wantErr: true,
expectedErrPrefix: `required flag(s) "gateway-ctlr-name" not set`,
},
{
name: "gateway-ctlr-name is set to empty string",
args: []string{
"--gateway-ctlr-name=",
"--gatewayclass=nginx",
},
wantErr: true,
expectedErrPrefix: `invalid argument "" for "--gateway-ctlr-name" flag: must be set`,
},
{
name: "gateway-ctlr-name is invalid",
args: []string{
"--gateway-ctlr-name=nginx-gateway",
"--gatewayclass=nginx",
},
wantErr: true,
expectedErrPrefix: `invalid argument "nginx-gateway" for "--gateway-ctlr-name" flag: invalid format; ` +
"must be DOMAIN/PATH",
},
{
name: "gatewayclass is not set",
args: []string{
"--gateway-ctlr-name=k8s-gateway.nginx.org/nginx-gateway",
},
wantErr: true,
expectedErrPrefix: `required flag(s) "gatewayclass" not set`,
},
{
name: "gatewayclass is set to empty string",
args: []string{
"--gateway-ctlr-name=k8s-gateway.nginx.org/nginx-gateway",
"--gatewayclass=",
},
wantErr: true,
expectedErrPrefix: `invalid argument "" for "--gatewayclass" flag: must be set`,
},
{
name: "gatewayclass is invalid",
args: []string{
"--gateway-ctlr-name=k8s-gateway.nginx.org/nginx-gateway",
"--gatewayclass=@",
},
wantErr: true,
expectedErrPrefix: `invalid argument "@" for "--gatewayclass" flag: invalid format`,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
g := NewGomegaWithT(t)

rootCmd := createRootCommand()
// discard any output generated by cobra
rootCmd.SetOut(io.Discard)
rootCmd.SetErr(io.Discard)

rootCmd.SetArgs(test.args)
err := rootCmd.Execute()

if test.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(HavePrefix(test.expectedErrPrefix))
} else {
g.Expect(err).ToNot(HaveOccurred())
}
})
}
}
72 changes: 6 additions & 66 deletions cmd/gateway/main.go
Original file line number Diff line number Diff line change
@@ -1,87 +1,27 @@
package main

import (
"errors"
"fmt"
"net"
"os"

flag "github.com/spf13/pflag"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/nginxinc/nginx-kubernetes-gateway/internal/config"
"github.com/nginxinc/nginx-kubernetes-gateway/internal/manager"
)

const (
domain = "k8s-gateway.nginx.org"
gatewayClassNameUsage = `The name of the GatewayClass resource. ` +
`Every NGINX Gateway must have a unique corresponding GatewayClass resource.`
gatewayCtrlNameUsageFmt = `The name of the Gateway controller. ` +
`The controller name must be of the form: DOMAIN/PATH. The controller's domain is '%s'`
)

var (
// Set during go build
version string
commit string
date string

// Command-line flags
gatewayCtlrName = flag.String(
"gateway-ctlr-name",
"",
fmt.Sprintf(gatewayCtrlNameUsageFmt, domain),
)

gatewayClassName = flag.String("gatewayclass", "", gatewayClassNameUsage)

// Environment variables
podIP = os.Getenv("POD_IP")
)

func validateIP(ip string) error {
if ip == "" {
return errors.New("IP address must be set")
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("%q must be a valid IP address", ip)
}

return nil
}

func main() {
flag.Parse()

MustValidateArguments(
flag.CommandLine,
GatewayControllerParam(domain),
GatewayClassParam(),
)

if err := validateIP(podIP); err != nil {
fmt.Printf("error validating POD_IP environment variable: %v\n", err)
os.Exit(1)
}

logger := zap.New()
conf := config.Config{
GatewayCtlrName: *gatewayCtlrName,
Logger: logger,
GatewayClassName: *gatewayClassName,
PodIP: podIP,
}
rootCmd := createRootCommand()

logger.Info("Starting NGINX Kubernetes Gateway",
"version", version,
"commit", commit,
"date", date,
rootCmd.AddCommand(
createStaticModeCommand(),
createProvisionerModeCommand(),
)

err := manager.Start(conf)
if err != nil {
logger.Error(err, "Failed to start control loop")
if err := rootCmd.Execute(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
47 changes: 0 additions & 47 deletions cmd/gateway/main_test.go

This file was deleted.

Loading

0 comments on commit dc046cc

Please sign in to comment.