Skip to content

Commit 9c2a61b

Browse files
pmalekczeslavo
andauthored
feat: add Config.Validate() to allow validation of configuration options before starting the manager (#3405)
* feat: add Config.Validate() to allow validation of configuration options before starting the manager * feat: add config validation unit tests and FlagNamespacedName * lint Co-authored-by: Grzegorz Burzyński <[email protected]>
1 parent 08201d0 commit 9c2a61b

File tree

11 files changed

+279
-119
lines changed

11 files changed

+279
-119
lines changed

internal/cmd/rootcmd/rootcmd.go

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,49 @@ import (
1414
// Execute is the entry point to the controller manager.
1515
func Execute() {
1616
var (
17-
cfg manager.Config
18-
rootCmd = &cobra.Command{
19-
PersistentPreRunE: bindEnvVars,
20-
RunE: func(cmd *cobra.Command, args []string) error {
21-
return Run(cmd.Context(), &cfg)
22-
},
23-
SilenceUsage: true,
24-
// We can silence the errors because cobra.CheckErr below will print
25-
// the returned error and set the exit code to 1.
26-
SilenceErrors: true,
27-
}
28-
versionCmd = &cobra.Command{
29-
Use: "version",
30-
Short: "Show JSON version information",
31-
RunE: func(cmd *cobra.Command, args []string) error {
32-
type Version struct {
33-
Release string `json:"release"`
34-
Repo string `json:"repo"`
35-
Commit string `json:"commit"`
36-
}
37-
out, err := json.Marshal(Version{
38-
Release: metadata.Release,
39-
Repo: metadata.Repo,
40-
Commit: metadata.Commit,
41-
})
42-
if err != nil {
43-
return fmt.Errorf("failed to print version information: %w", err)
44-
}
45-
fmt.Printf("%s\n", out)
46-
return nil
47-
},
48-
}
17+
cfg manager.Config
18+
rootCmd = GetRootCmd(&cfg)
19+
versionCmd = GetVersionCmd()
4920
)
5021
rootCmd.AddCommand(versionCmd)
51-
rootCmd.Flags().AddFlagSet(cfg.FlagSet())
5222
cobra.CheckErr(rootCmd.Execute())
5323
}
24+
25+
func GetRootCmd(cfg *manager.Config) *cobra.Command {
26+
cmd := &cobra.Command{
27+
PersistentPreRunE: bindEnvVars,
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
return Run(cmd.Context(), cfg)
30+
},
31+
SilenceUsage: true,
32+
// We can silence the errors because cobra.CheckErr below will print
33+
// the returned error and set the exit code to 1.
34+
SilenceErrors: true,
35+
}
36+
cmd.Flags().AddFlagSet(cfg.FlagSet())
37+
return cmd
38+
}
39+
40+
func GetVersionCmd() *cobra.Command {
41+
return &cobra.Command{
42+
Use: "version",
43+
Short: "Show JSON version information",
44+
RunE: func(cmd *cobra.Command, args []string) error {
45+
type Version struct {
46+
Release string `json:"release"`
47+
Repo string `json:"repo"`
48+
Commit string `json:"commit"`
49+
}
50+
out, err := json.Marshal(Version{
51+
Release: metadata.Release,
52+
Repo: metadata.Repo,
53+
Commit: metadata.Commit,
54+
})
55+
if err != nil {
56+
return fmt.Errorf("failed to print version information: %w", err)
57+
}
58+
fmt.Printf("%s\n", out)
59+
return nil
60+
},
61+
}
62+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package rootcmd
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/kong/kubernetes-ingress-controller/v2/internal/manager"
10+
)
11+
12+
func TestRootCmd(t *testing.T) {
13+
t.Run("root command succeeds by default", func(t *testing.T) {
14+
var cfg manager.Config
15+
rootCmd := GetRootCmd(&cfg)
16+
require.NoError(t, rootCmd.PersistentPreRunE(rootCmd, os.Args[:0]))
17+
})
18+
19+
t.Run("root command succeeds when correct flags where provided", func(t *testing.T) {
20+
var cfg manager.Config
21+
rootCmd := GetRootCmd(&cfg)
22+
require.NoError(t, rootCmd.PersistentPreRunE(rootCmd,
23+
append(os.Args[:0],
24+
"--publish-service", "namespace/servicename",
25+
),
26+
))
27+
})
28+
29+
t.Run("binding environment variables succeeds when flag validation passes", func(t *testing.T) {
30+
t.Setenv("CONTROLLER_PUBLISH_SERVICE", "namespace/servicename")
31+
var cfg manager.Config
32+
rootCmd := GetRootCmd(&cfg)
33+
require.NoError(t, rootCmd.PersistentPreRunE(rootCmd, os.Args[:0]))
34+
})
35+
36+
t.Run("binding environment variables fails when flag validation fails", func(t *testing.T) {
37+
t.Setenv("CONTROLLER_PUBLISH_SERVICE", "servicename")
38+
var cfg manager.Config
39+
rootCmd := GetRootCmd(&cfg)
40+
require.Error(t, rootCmd.PersistentPreRunE(rootCmd, os.Args[:0]),
41+
"binding env vars should fail because a non namespaced name of publish service was provided",
42+
)
43+
})
44+
}

internal/cmd/rootcmd/run.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ func RunWithLogger(ctx context.Context, c *manager.Config, deprecatedLogger logr
2828
return fmt.Errorf("failed to setup signal handler: %w", err)
2929
}
3030

31+
if err := c.Validate(); err != nil {
32+
return fmt.Errorf("failed to validate config: %w", err)
33+
}
34+
3135
diag, err := StartDiagnosticsServer(ctx, manager.DiagnosticsPort, c, logger)
3236
if err != nil {
3337
return fmt.Errorf("failed to start diagnostics server: %w", err)
3438
}
39+
3540
return manager.Run(ctx, c, diag.ConfigDumps, deprecatedLogger)
3641
}

internal/manager/config.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package manager
33
import (
44
"context"
55
"fmt"
6+
"regexp"
67
"time"
78

89
"github.com/kong/go-kong/kong"
@@ -57,7 +58,6 @@ type Config struct {
5758
// Kubernetes configurations
5859
KubeconfigPath string
5960
IngressClassName string
60-
EnableLeaderElection bool
6161
LeaderElectionNamespace string
6262
LeaderElectionID string
6363
Concurrency int
@@ -66,7 +66,7 @@ type Config struct {
6666
GatewayAPIControllerName string
6767

6868
// Ingress status
69-
PublishService string
69+
PublishService FlagNamespacedName
7070
PublishStatusAddress []string
7171
UpdateStatus bool
7272

@@ -101,15 +101,27 @@ type Config struct {
101101
// helpful for advanced cases with load-balancers so that the ingress
102102
// controller can be gracefully removed/drained from their rotation.
103103
TermDelay time.Duration
104+
105+
flagSet *pflag.FlagSet
104106
}
105107

106108
// -----------------------------------------------------------------------------
107109
// Controller Manager - Config - Methods
108110
// -----------------------------------------------------------------------------
109111

112+
// Validate validates the config. With time this logic may grow to invalidate
113+
// incorrect configurations.
114+
func (c *Config) Validate() error {
115+
if !isControllerNameValid(c.GatewayAPIControllerName) {
116+
return fmt.Errorf("--gateway-api-controller-name (%s) is invalid. The expected format is example.com/controller-name", c.GatewayAPIControllerName)
117+
}
118+
119+
return nil
120+
}
121+
110122
// FlagSet binds the provided Config to commandline flags.
111123
func (c *Config) FlagSet() *pflag.FlagSet {
112-
flagSet := pflag.NewFlagSet("", pflag.ExitOnError)
124+
flagSet := pflag.NewFlagSet("", pflag.ContinueOnError)
113125

114126
// Logging configurations
115127
flagSet.StringVar(&c.LogLevel, "log-level", "info", `Level of logging for the controller. Allowed values are trace, debug, info, warn, error, fatal and panic.`)
@@ -163,11 +175,10 @@ func (c *Config) FlagSet() *pflag.FlagSet {
163175
flagSet.StringSliceVar(&c.FilterTags, "kong-admin-filter-tag", []string{"managed-by-ingress-controller"}, "The tag used to manage and filter entities in Kong. This flag can be specified multiple times to specify multiple tags. This setting will be silently ignored if the Kong instance has no tags support.")
164176
flagSet.IntVar(&c.Concurrency, "kong-admin-concurrency", 10, "Max number of concurrent requests sent to Kong's Admin API.")
165177
flagSet.StringSliceVar(&c.WatchNamespaces, "watch-namespace", nil,
166-
`Namespace(s) to watch for Kubernetes resources. Defaults to all namespaces.`+
167-
`To watch multiple namespaces, use a comma-separated list of namespaces.`)
178+
`Namespace(s) to watch for Kubernetes resources. Defaults to all namespaces. To watch multiple namespaces, use a comma-separated list of namespaces.`)
168179

169180
// Ingress status
170-
flagSet.StringVar(&c.PublishService, "publish-service", "",
181+
flagSet.Var(&c.PublishService, "publish-service",
171182
`Service fronting Ingress resources in "namespace/name" format. The controller will update Ingress status information with this Service's endpoints.`)
172183
flagSet.StringSliceVar(&c.PublishStatusAddress, "publish-status-address", []string{},
173184
`User-provided addresses in comma-separated string format, for use in lieu of "publish-service" `+
@@ -218,7 +229,7 @@ func (c *Config) FlagSet() *pflag.FlagSet {
218229

219230
// Deprecated flags
220231

221-
flagSet.Float32Var(&c.ProxySyncSeconds, "sync-rate-limit", dataplane.DefaultSyncSeconds, "Use --proxy-sync-seconds instead")
232+
_ = flagSet.Float32("sync-rate-limit", dataplane.DefaultSyncSeconds, "Use --proxy-sync-seconds instead")
222233
_ = flagSet.MarkDeprecated("sync-rate-limit", "Use --proxy-sync-seconds instead")
223234

224235
_ = flagSet.Int("stderrthreshold", 0, "Has no effect and will be removed in future releases (see github issue #1297)")
@@ -230,9 +241,10 @@ func (c *Config) FlagSet() *pflag.FlagSet {
230241
_ = flagSet.String("kong-custom-entities-secret", "", "Will be removed in next major release.")
231242
_ = flagSet.MarkDeprecated("kong-custom-entities-secret", "Will be removed in next major release.")
232243

233-
flagSet.BoolVar(&c.EnableLeaderElection, "leader-elect", false, "DEPRECATED as of 2.1.0 leader election behavior is determined automatically and this flag has no effect")
234-
_ = flagSet.MarkDeprecated("leader-elect", "DEPRECATED as of 2.1.0 leader election behavior is determined automatically and this flag has no effect")
244+
_ = flagSet.Bool("leader-elect", false, "DEPRECATED as of 2.1.0: leader election behavior is determined automatically based on the Kong database setting and this flag has no effect")
245+
_ = flagSet.MarkDeprecated("leader-elect", "DEPRECATED as of 2.1.0: leader election behavior is determined automatically based on the Kong database setting and this flag has no effect")
235246

247+
c.flagSet = flagSet
236248
return flagSet
237249
}
238250

@@ -277,3 +289,9 @@ func (c *Config) GetKubeClient() (client.Client, error) {
277289
}
278290
return client.New(conf, client.Options{})
279291
}
292+
293+
func isControllerNameValid(controllerName string) bool {
294+
// https://github.com/kubernetes-sigs/gateway-api/blob/547122f7f55ac0464685552898c560658fb40073/apis/v1beta1/shared_types.go#L448-L463
295+
re := regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$`)
296+
return re.Match([]byte(controllerName))
297+
}

internal/manager/config_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package manager
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestConfigValidate(t *testing.T) {
11+
t.Run("--gateway-api-controller-name", func(t *testing.T) {
12+
t.Run("valid config", func(t *testing.T) {
13+
var c Config
14+
require.NoError(t, c.FlagSet().Parse(
15+
[]string{
16+
os.Args[0],
17+
`--gateway-api-controller-name`, `example.com/controller-name`,
18+
},
19+
))
20+
require.NoError(t, c.Validate())
21+
})
22+
23+
t.Run("invalid config", func(t *testing.T) {
24+
var c Config
25+
require.NoError(t, c.FlagSet().Parse(
26+
[]string{
27+
os.Args[0],
28+
`--gateway-api-controller-name`, `%invalid_controller_name$`,
29+
},
30+
))
31+
require.Error(t, c.Validate())
32+
})
33+
})
34+
35+
t.Run("--publish-service", func(t *testing.T) {
36+
t.Run("valid config", func(t *testing.T) {
37+
var c Config
38+
require.NoError(t, c.FlagSet().Parse(
39+
[]string{
40+
os.Args[0],
41+
`--publish-service`, `namespace/servicename`,
42+
},
43+
))
44+
require.NoError(t, c.Validate())
45+
})
46+
47+
t.Run("invalid config", func(t *testing.T) {
48+
var c Config
49+
require.Error(t, c.FlagSet().Parse(
50+
[]string{
51+
os.Args[0],
52+
`--publish-service`, `servicename`,
53+
},
54+
))
55+
// publish service is validated through FlagNamespacedName validation logic.
56+
require.NoError(t, c.Validate())
57+
})
58+
})
59+
}

internal/manager/controllerdef.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ func setupControllers(
360360
Log: ctrl.Log.WithName("controllers").WithName(gatewayFeature),
361361
Scheme: mgr.GetScheme(),
362362
DataplaneClient: dataplaneClient,
363-
PublishService: c.PublishService,
363+
PublishService: c.PublishService.String(),
364364
WatchNamespaces: c.WatchNamespaces,
365365
EnableReferenceGrant: referenceGrantsEnabled,
366366
CacheSyncTimeout: c.CacheSyncTimeout,

internal/manager/feature_gates.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ const (
3131
)
3232

3333
// setupFeatureGates converts feature gates to controller enablement.
34-
func setupFeatureGates(setupLog logr.Logger, c *Config) (map[string]bool, error) {
34+
func setupFeatureGates(setupLog logr.Logger, featureGates map[string]bool) (map[string]bool, error) {
3535
// generate a map of feature gates by string names to their controller enablement
3636
ctrlMap := getFeatureGatesDefaults()
3737

3838
// override the default settings
39-
for feature, enabled := range c.FeatureGates {
39+
for feature, enabled := range featureGates {
4040
setupLog.Info("found configuration option for gated feature", "feature", feature, "enabled", enabled)
4141
_, ok := ctrlMap[feature]
4242
if !ok {

internal/manager/feature_gates_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,23 @@ func TestFeatureGates(t *testing.T) {
1616
baseLogger.SetOutput(out)
1717
baseLogger.SetLevel(logrus.DebugLevel)
1818
setupLog := logrusr.New(baseLogger)
19-
config := new(Config)
2019

2120
t.Log("verifying feature gates setup defaults when no feature gates are configured")
22-
fgs, err := setupFeatureGates(setupLog, config)
21+
fgs, err := setupFeatureGates(setupLog, nil)
2322
assert.NoError(t, err)
2423
assert.Len(t, fgs, len(getFeatureGatesDefaults()))
2524

2625
t.Log("verifying feature gates setup results when valid feature gates options are present")
27-
config.FeatureGates = map[string]bool{gatewayFeature: true}
28-
fgs, err = setupFeatureGates(setupLog, config)
26+
featureGates := map[string]bool{gatewayFeature: true}
27+
fgs, err = setupFeatureGates(setupLog, featureGates)
2928
assert.NoError(t, err)
3029
assert.True(t, fgs[gatewayFeature])
3130

3231
t.Log("configuring several invalid feature gates options")
33-
config.FeatureGates = map[string]bool{"invalidGateway": true}
32+
featureGates = map[string]bool{"invalidGateway": true}
3433

3534
t.Log("verifying feature gates setup results when invalid feature gates options are present")
36-
_, err = setupFeatureGates(setupLog, config)
35+
_, err = setupFeatureGates(setupLog, featureGates)
3736
assert.Error(t, err)
3837
assert.Contains(t, err.Error(), "invalidGateway is not a valid feature")
3938
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package manager
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"k8s.io/apimachinery/pkg/types"
8+
)
9+
10+
// FlagNamespacedName allows parsing command line flags straight to types.NamespacedName.
11+
type FlagNamespacedName struct {
12+
NN types.NamespacedName
13+
}
14+
15+
func (f *FlagNamespacedName) String() string {
16+
return f.NN.String()
17+
}
18+
19+
func (f *FlagNamespacedName) Set(v string) error {
20+
s := strings.SplitN(v, "/", 3)
21+
if len(s) != 2 {
22+
return fmt.Errorf("namespaced name should be in the format: <namespace>/<name>")
23+
}
24+
f.NN = types.NamespacedName{
25+
Namespace: s[0],
26+
Name: s[1],
27+
}
28+
return nil
29+
}
30+
31+
func (f *FlagNamespacedName) Type() string {
32+
return "FlagNamespacedName"
33+
}

0 commit comments

Comments
 (0)