Skip to content

Commit 95c97ce

Browse files
authored
refactor: replace config validation with generic ValidatedValue implementation (#3415)
1 parent 2946389 commit 95c97ce

File tree

9 files changed

+268
-107
lines changed

9 files changed

+268
-107
lines changed

internal/manager/config.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/kong/go-kong/kong"
1010
"github.com/spf13/pflag"
11+
"k8s.io/apimachinery/pkg/types"
1112
"k8s.io/client-go/rest"
1213
"k8s.io/client-go/tools/clientcmd"
1314
cliflag "k8s.io/component-base/cli/flag"
@@ -66,7 +67,7 @@ type Config struct {
6667
GatewayAPIControllerName string
6768

6869
// Ingress status
69-
PublishService FlagNamespacedName
70+
PublishService types.NamespacedName
7071
PublishStatusAddress []string
7172
UpdateStatus bool
7273

@@ -109,13 +110,9 @@ type Config struct {
109110
// Controller Manager - Config - Methods
110111
// -----------------------------------------------------------------------------
111112

112-
// Validate validates the config. With time this logic may grow to invalidate
113-
// incorrect configurations.
113+
// Validate validates the config. It should be used to validate the config variables' interdependencies.
114+
// When a single variable is to be validated, NewValidatedValue should be used.
114115
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-
119116
return nil
120117
}
121118

@@ -167,7 +164,7 @@ func (c *Config) FlagSet() *pflag.FlagSet {
167164
)
168165

169166
// Kubernetes configurations
170-
flagSet.StringVar(&c.GatewayAPIControllerName, "gateway-api-controller-name", string(gateway.ControllerName), "The controller name to match on Gateway API resources.")
167+
flagSet.Var(NewValidatedValueWithDefault(&c.GatewayAPIControllerName, gatewayAPIControllerNameFromFlagValue, string(gateway.ControllerName)), "gateway-api-controller-name", "The controller name to match on Gateway API resources.")
171168
flagSet.StringVar(&c.KubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file.")
172169
flagSet.StringVar(&c.IngressClassName, "ingress-class", annotations.DefaultIngressClass, `Name of the ingress class to route through this controller.`)
173170
flagSet.StringVar(&c.LeaderElectionID, "election-id", "5b374a9e.konghq.com", `Election id to use for status update.`)
@@ -178,7 +175,7 @@ func (c *Config) FlagSet() *pflag.FlagSet {
178175
`Namespace(s) to watch for Kubernetes resources. Defaults to all namespaces. To watch multiple namespaces, use a comma-separated list of namespaces.`)
179176

180177
// Ingress status
181-
flagSet.Var(&c.PublishService, "publish-service",
178+
flagSet.Var(NewValidatedValue(&c.PublishService, namespacedNameFromFlagValue), "publish-service",
182179
`Service fronting Ingress resources in "namespace/name" format. The controller will update Ingress status information with this Service's endpoints.`)
183180
flagSet.StringSliceVar(&c.PublishStatusAddress, "publish-status-address", []string{},
184181
`User-provided addresses in comma-separated string format, for use in lieu of "publish-service" `+

internal/manager/config_test.go

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package manager
2+
3+
import (
4+
"errors"
5+
"strings"
6+
7+
"k8s.io/apimachinery/pkg/types"
8+
)
9+
10+
// This file contains a set of constructors that are used to validate and set validated values in Config.
11+
// They're meant to be used together with ValidatedValue[T] type.
12+
13+
func namespacedNameFromFlagValue(flagValue string) (types.NamespacedName, error) {
14+
parts := strings.SplitN(flagValue, "/", 3)
15+
if len(parts) != 2 {
16+
return types.NamespacedName{}, errors.New("the expected format is namespace/name")
17+
}
18+
return types.NamespacedName{
19+
Namespace: parts[0],
20+
Name: parts[1],
21+
}, nil
22+
}
23+
24+
func gatewayAPIControllerNameFromFlagValue(flagValue string) (string, error) {
25+
if !isControllerNameValid(flagValue) {
26+
return "", errors.New("the expected format is example.com/controller-name")
27+
}
28+
return flagValue, nil
29+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package manager
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"k8s.io/apimachinery/pkg/types"
9+
10+
"github.com/kong/kubernetes-ingress-controller/v2/internal/controllers/gateway"
11+
)
12+
13+
func TestConfigValidatedVars(t *testing.T) {
14+
type testCase struct {
15+
Input string
16+
ExpectedValue any
17+
ExtractValueFn func(c Config) any
18+
ExpectedErrorContains string
19+
}
20+
21+
testCasesGroupedByFlag := map[string][]testCase{
22+
"--gateway-api-controller-name": {
23+
{
24+
Input: "example.com/controller-name",
25+
ExtractValueFn: func(c Config) any {
26+
return c.GatewayAPIControllerName
27+
},
28+
ExpectedValue: "example.com/controller-name",
29+
},
30+
{
31+
Input: "",
32+
ExtractValueFn: func(c Config) any {
33+
return c.GatewayAPIControllerName
34+
},
35+
ExpectedValue: string(gateway.ControllerName),
36+
},
37+
{
38+
Input: "%invalid_controller_name$",
39+
ExpectedErrorContains: "the expected format is example.com/controller-name",
40+
},
41+
},
42+
"--publish-service": {
43+
{
44+
Input: "namespace/servicename",
45+
ExtractValueFn: func(c Config) any {
46+
return c.PublishService
47+
},
48+
ExpectedValue: types.NamespacedName{Namespace: "namespace", Name: "servicename"},
49+
},
50+
{
51+
Input: "servicename",
52+
ExpectedErrorContains: "the expected format is namespace/name",
53+
},
54+
},
55+
}
56+
57+
for flag, flagTestCases := range testCasesGroupedByFlag {
58+
for _, tc := range flagTestCases {
59+
t.Run(fmt.Sprintf("%s=%s", flag, tc.Input), func(t *testing.T) {
60+
var c Config
61+
var input []string
62+
if tc.Input != "" {
63+
input = []string{flag, tc.Input}
64+
}
65+
66+
err := c.FlagSet().Parse(input)
67+
if tc.ExpectedErrorContains != "" {
68+
require.ErrorContains(t, err, tc.ExpectedErrorContains)
69+
return
70+
}
71+
72+
require.NoError(t, err)
73+
require.Equal(t, tc.ExpectedValue, tc.ExtractValueFn(c))
74+
})
75+
}
76+
}
77+
}

internal/manager/flagnamespacedname.go

Lines changed: 0 additions & 33 deletions
This file was deleted.

internal/manager/run.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,6 @@ func Run(ctx context.Context, c *Config, diagnostic util.ConfigDumpDiagnostic, d
135135
return err
136136
}
137137

138-
if !isControllerNameValid(c.GatewayAPIControllerName) {
139-
return errors.New("--gateway-api-controller-name is invalid. The expected format is example.com/controller-name")
140-
}
141138
gateway.ControllerName = gatewayv1beta1.GatewayController(c.GatewayAPIControllerName)
142139

143140
setupLog.Info("Starting Enabled Controllers")

internal/manager/setup.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ func setupControllerOptions(logger logr.Logger, c *Config, dbmode string) (ctrl.
9292

9393
// if publish service has been provided the namespace for it should be
9494
// watched so that controllers can see updates to the service.
95-
if c.PublishService.NN.String() != "" {
96-
watchNamespaces = append(c.WatchNamespaces, c.PublishService.NN.Namespace)
95+
if c.PublishService.String() != "" {
96+
watchNamespaces = append(c.WatchNamespaces, c.PublishService.Namespace)
9797
}
9898
controllerOpts.NewCache = cache.MultiNamespacedCacheBuilder(watchNamespaces)
9999
}
@@ -192,7 +192,7 @@ func setupDataplaneAddressFinder(ctx context.Context, mgrc client.Client, c *Con
192192
if overrideAddrs := c.PublishStatusAddress; len(overrideAddrs) > 0 {
193193
dataplaneAddressFinder.SetOverrides(overrideAddrs)
194194
} else if c.PublishService.String() != "" {
195-
publishServiceNn := c.PublishService.NN
195+
publishServiceNn := c.PublishService
196196
dataplaneAddressFinder.SetGetter(func() ([]string, error) {
197197
svc := new(corev1.Service)
198198
if err := mgrc.Get(ctx, publishServiceNn, svc); err != nil {

internal/manager/validated.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package manager
2+
3+
import "fmt"
4+
5+
// ValidatedValue implements `pflag.Value` interface. It can be used for hooking up arbitrary validation logic to any type.
6+
// It should be passed to `pflag.FlagSet.Var()`.
7+
type ValidatedValue[T any] struct {
8+
origin string
9+
variable *T
10+
constructor func(string) (T, error)
11+
}
12+
13+
// NewValidatedValue creates a validated variable of type T. Constructor should validate the input and return an error
14+
// in case of any failures. If validation passes, constructor should return a value that's to be set in the variable.
15+
// The constructor accepts a flagValue that is raw input from user's command line (or an env variable that was bind to
16+
// the flag, see: bindEnvVars).
17+
func NewValidatedValue[T any](variable *T, constructor func(flagValue string) (T, error)) ValidatedValue[T] {
18+
return ValidatedValue[T]{
19+
constructor: constructor,
20+
variable: variable,
21+
}
22+
}
23+
24+
// NewValidatedValueWithDefault creates a validated variable of type T with a default value.
25+
func NewValidatedValueWithDefault[T any](variable *T, constructor func(flagValue string) (T, error), value T) ValidatedValue[T] {
26+
*variable = value
27+
return ValidatedValue[T]{
28+
constructor: constructor,
29+
variable: variable,
30+
}
31+
}
32+
33+
func (v ValidatedValue[T]) String() string {
34+
return v.origin
35+
}
36+
37+
func (v ValidatedValue[T]) Set(s string) error {
38+
value, err := v.constructor(s)
39+
if err != nil {
40+
return err
41+
}
42+
43+
*v.variable = value
44+
return nil
45+
}
46+
47+
func (v ValidatedValue[T]) Type() string {
48+
var t T
49+
return fmt.Sprintf("Validated%T", t)
50+
}

0 commit comments

Comments
 (0)