Skip to content

Commit a715af3

Browse files
author
Per Goncalves da Silva
committed
Add config support with jsonschema validation
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent e88a6ea commit a715af3

File tree

5 files changed

+570
-7
lines changed

5 files changed

+570
-7
lines changed

go.mod

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ require (
1717
github.com/google/go-containerregistry v0.20.6
1818
github.com/google/renameio/v2 v2.0.0
1919
github.com/gorilla/handlers v1.5.2
20+
github.com/invopop/jsonschema v0.13.0
2021
github.com/klauspost/compress v1.18.0
2122
github.com/opencontainers/go-digest v1.0.0
2223
github.com/opencontainers/image-spec v1.1.1
2324
github.com/operator-framework/api v0.34.0
2425
github.com/operator-framework/helm-operator-plugins v0.8.0
2526
github.com/operator-framework/operator-registry v1.57.0
26-
github.com/prometheus/client_golang v1.23.2
27-
github.com/prometheus/common v0.66.1
27+
github.com/prometheus/client_golang v1.23.0
28+
github.com/prometheus/common v0.65.0
29+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
2830
github.com/spf13/cobra v1.10.1
2931
github.com/stretchr/testify v1.11.1
3032
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
@@ -69,7 +71,9 @@ require (
6971
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
7072
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
7173
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
74+
github.com/bahlo/generic-list-go v0.2.0 // indirect
7275
github.com/beorn7/perks v1.0.1 // indirect
76+
github.com/buger/jsonparser v1.1.1 // indirect
7377
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
7478
github.com/cespare/xxhash/v2 v2.3.0 // indirect
7579
github.com/chai2010/gettext-go v1.0.2 // indirect
@@ -182,7 +186,6 @@ require (
182186
github.com/rivo/uniseg v0.4.7 // indirect
183187
github.com/rubenv/sql-migrate v1.8.0 // indirect
184188
github.com/russross/blackfriday/v2 v2.1.0 // indirect
185-
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
186189
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
187190
github.com/shopspring/decimal v1.4.0 // indirect
188191
github.com/sigstore/fulcio v1.7.1 // indirect
@@ -199,6 +202,7 @@ require (
199202
github.com/ulikunitz/xz v0.5.14 // indirect
200203
github.com/vbatts/tar-split v0.12.1 // indirect
201204
github.com/vbauerster/mpb/v8 v8.10.2 // indirect
205+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
202206
github.com/x448/float16 v0.8.4 // indirect
203207
github.com/xlab/treeprint v1.2.0 // indirect
204208
go.etcd.io/bbolt v1.4.3 // indirect

go.sum

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
3838
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
3939
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
4040
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
41+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
42+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
4143
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
4244
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
4345
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
4446
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
4547
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
4648
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
49+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
50+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
4751
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
4852
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
4953
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -271,6 +275,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
271275
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
272276
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
273277
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
278+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
279+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
274280
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
275281
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
276282
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
@@ -399,13 +405,13 @@ github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
399405
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
400406
github.com/proglottis/gpgme v0.1.4 h1:3nE7YNA70o2aLjcg63tXMOhPD7bplfE5CBdV+hLAm2M=
401407
github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM=
402-
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
403-
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
408+
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
409+
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
404410
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
405411
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
406412
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
407-
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
408-
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
413+
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
414+
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
409415
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
410416
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
411417
github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 h1:uTiEyEyfLhkw678n6EulHVto8AkcXVr8zUcBJNZ0ark=
@@ -473,6 +479,8 @@ github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnn
473479
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
474480
github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM=
475481
github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0=
482+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
483+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
476484
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
477485
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
478486
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/config",
4+
"properties":
5+
{
6+
"watchNamespace":
7+
{
8+
"type": "string"
9+
}
10+
},
11+
"additionalProperties": false,
12+
"type": "object"
13+
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package bundlecfg
2+
3+
import (
4+
_ "embed"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"slices"
9+
"strings"
10+
11+
"github.com/invopop/jsonschema"
12+
schemavalidation "github.com/santhosh-tekuri/jsonschema/v6"
13+
"github.com/santhosh-tekuri/jsonschema/v6/kind"
14+
"k8s.io/apimachinery/pkg/util/sets"
15+
"k8s.io/apimachinery/pkg/util/validation"
16+
17+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
18+
19+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
20+
)
21+
22+
const (
23+
dns1123SubdomainFormat = "Namespace"
24+
notOwnNamespaceFormat = "NotOwnNamespace"
25+
)
26+
27+
var (
28+
//go:embed bundle_config_schema.json
29+
// bundleConfigBaseSchema is the base jsonschema for a registry+v1 bundle configuration
30+
// The rool level properties (e.g. watchNamespaces) must match the attribute names in the
31+
// Config struct's properties' json annotations. The final schema can be mutated to respect
32+
// bundle specific settings, e.g. the particular install mode support (i.e. if the bundle
33+
// only supports AllNamespaces install mode, it doesn't need a 'watchNamespace' parameter).
34+
// TODO: when we are ready to develop the SubscriptionConfig support, update Config with
35+
// the *v1alpha1.SubscriptionConfig parameter, and update the base schema with the json value
36+
// of the 'config' parameter in the SubscriptionConfig CRD found here:
37+
// https://github.com/operator-framework/api/blob/master/crds/operators.coreos.com_subscriptions.yaml#L70
38+
bundleConfigBaseSchema []byte
39+
40+
// supportedBundleInstallModes is a set of install modes supported by OLMv1
41+
supportedBundleInstallModes = sets.New[v1alpha1.InstallModeType](
42+
v1alpha1.InstallModeTypeAllNamespaces,
43+
v1alpha1.InstallModeTypeSingleNamespace,
44+
v1alpha1.InstallModeTypeOwnNamespace,
45+
)
46+
47+
// dnsFormat checks conformity to RFC1213 lowercase dns subdomain format by any field with format 'RFC-1123'
48+
dnsFormat = &schemavalidation.Format{
49+
Name: dns1123SubdomainFormat,
50+
Validate: func(v any) error {
51+
if v == nil {
52+
return nil
53+
}
54+
s, ok := v.(string)
55+
if !ok {
56+
return fmt.Errorf("invalid type %T, expected string", v)
57+
}
58+
errs := validation.IsDNS1123Subdomain(s)
59+
if len(errs) > 0 {
60+
return fmt.Errorf("%q is not a valid namespace name: %s", v, strings.Join(errs, ", "))
61+
}
62+
return nil
63+
},
64+
}
65+
)
66+
67+
// Config is a registry+v1 bundle configuration surface
68+
type Config struct {
69+
// WatchNamespace is supported for certain bundles to allow the user to configure installation in Single- or OwnNamespace modes
70+
// The validation behavior of this field is determined by the install modes supported by the bundle, e.g.:
71+
// - If a bundle only supports AllNamespaces mode (or only OwnNamespace mode): this field will be unknown
72+
// - If a bundle supports AllNamespaces and SingleNamespace install modes: this field is optional
73+
// - If a bundle supports AllNamespaces and OwnNamespace: this field is optional, but if set must be equal to the install namespace
74+
WatchNamespace string `json:"watchNamespace,omitempty"`
75+
}
76+
77+
// ConfigSchema
78+
type ConfigSchema struct{}
79+
80+
// Unmarshall returns a validated Config struct from the values given in rawConfig.
81+
// The applied validation will be determined by the install modes supported by the bundle
82+
func Unmarshall(rv1 bundle.RegistryV1, installNamespace string, rawConfig map[string]interface{}) (*Config, error) {
83+
if len(rawConfig) == 0 {
84+
return nil, nil
85+
}
86+
87+
rawSchema, err := bundleConfigSchema(rv1, installNamespace)
88+
if err != nil {
89+
return nil, fmt.Errorf("error generating bundle config schema: %v", err)
90+
}
91+
92+
// custom formats used for field validation
93+
// for instance kubernetes namespace name.
94+
// Also used for value validation, e.g. when a watchNamespace cannot be the install namespace
95+
// because more control over the error message can be given
96+
customFormats := []*schemavalidation.Format{
97+
dnsFormat,
98+
notOwnNamespaceFmt(installNamespace),
99+
}
100+
101+
if err := validateBundleConfig(rawSchema, customFormats, rawConfig); err != nil {
102+
return nil, fmt.Errorf("invalid configuration: %v", err)
103+
}
104+
105+
return toConfig(rawConfig)
106+
}
107+
108+
// bundleConfigSchema generates a jsonschema used to validate bundle configuration
109+
func bundleConfigSchema(rv1 bundle.RegistryV1, installNamespace string) ([]byte, error) {
110+
schema := &jsonschema.Schema{}
111+
if err := json.Unmarshal(bundleConfigBaseSchema, schema); err != nil {
112+
return nil, err
113+
}
114+
115+
// apply bundle rawConfig based mutations for watchNamespace
116+
if err := configureWatchNamespaceProperty(rv1, installNamespace, schema); err != nil {
117+
return nil, err
118+
}
119+
120+
// return schema
121+
out, err := schema.MarshalJSON()
122+
if err != nil {
123+
panic(err)
124+
}
125+
return out, err
126+
}
127+
128+
// configureWatchNamespaceProperty modifies schema to configure the watchNamespace config property based on
129+
// the install modes supported by the bundle marking the field required or optional, or restricting the possible values
130+
// it can take
131+
func configureWatchNamespaceProperty(rv1 bundle.RegistryV1, installNamespace string, schema *jsonschema.Schema) error {
132+
bundleInstallModes := sets.New[v1alpha1.InstallModeType]()
133+
for _, im := range rv1.CSV.Spec.InstallModes {
134+
if im.Supported {
135+
bundleInstallModes.Insert(im.Type)
136+
}
137+
}
138+
139+
supportedInstallModes := bundleInstallModes.Intersection(supportedBundleInstallModes)
140+
141+
if len(supportedInstallModes) == 0 {
142+
//bundleModes := slices.Sorted(slices.Values(bundleInstallModes.UnsortedList()))
143+
supportedModes := slices.Sorted(slices.Values(supportedBundleInstallModes.UnsortedList()))
144+
return fmt.Errorf("bundle does not support any of the allowable install modes %v", supportedModes)
145+
}
146+
147+
allSupported := supportedInstallModes.Has(v1alpha1.InstallModeTypeAllNamespaces)
148+
singleSupported := supportedInstallModes.Has(v1alpha1.InstallModeTypeSingleNamespace)
149+
ownSupported := supportedInstallModes.Has(v1alpha1.InstallModeTypeOwnNamespace)
150+
151+
// no watchNamespace rawConfig parameter if bundle only supports AllNamespaces or OwnNamespace install modes
152+
if len(supportedInstallModes) == 1 && (allSupported || ownSupported) {
153+
schema.Properties.Delete("watchNamespace")
154+
return nil
155+
}
156+
157+
watchNamespaceProperty, ok := schema.Properties.Get("watchNamespace")
158+
if !ok {
159+
return errors.New("watchNamespace not found in schema")
160+
}
161+
162+
watchNamespaceProperty.Format = dns1123SubdomainFormat
163+
164+
// required or optional
165+
if !allSupported && singleSupported {
166+
schema.Required = append(schema.Required, "watchNamespace")
167+
} else {
168+
// note: the library currently doesn't support jsonschema.Types
169+
// this is the current workaround for declaring optional/nullable fields
170+
// https://github.com/invopop/jsonschema/issues/115
171+
watchNamespaceProperty.Extras = map[string]any{
172+
"type": []string{"string", "null"},
173+
}
174+
if !ownSupported {
175+
// if own namespace is not supported validate that it is not being used
176+
watchNamespaceProperty.Format = notOwnNamespaceFormat
177+
}
178+
}
179+
180+
// must be the install namespace
181+
if allSupported && ownSupported && !singleSupported {
182+
watchNamespaceProperty.Enum = []any{
183+
installNamespace,
184+
nil,
185+
}
186+
}
187+
return nil
188+
}
189+
190+
// validateBundleConfig validates the bundle rawConfig
191+
func validateBundleConfig(rawSchema []byte, customFormats []*schemavalidation.Format, rawConfig map[string]interface{}) error {
192+
schema, err := schemavalidation.UnmarshalJSON(strings.NewReader(string(rawSchema)))
193+
if err != nil {
194+
return err
195+
}
196+
197+
compiler := schemavalidation.NewCompiler()
198+
for _, format := range customFormats {
199+
compiler.RegisterFormat(format)
200+
}
201+
compiler.AssertFormat()
202+
if err := compiler.AddResource("schema.json", schema); err != nil {
203+
return err
204+
}
205+
compiledSchema, err := compiler.Compile("schema.json")
206+
if err != nil {
207+
return err
208+
}
209+
210+
return formatJSONSchemaValidationError(compiledSchema.Validate(rawConfig))
211+
}
212+
213+
// toConfig converts rawConfig into a Config struct
214+
func toConfig(rawConfig map[string]interface{}) (*Config, error) {
215+
bytes, err := json.Marshal(rawConfig)
216+
if err != nil {
217+
return nil, err
218+
}
219+
cfg := &Config{}
220+
err = json.Unmarshal(bytes, cfg)
221+
return cfg, err
222+
}
223+
224+
// formatJSONSchemaValidationError extracts and formats the jsonschema validation errors given by the underlying library
225+
func formatJSONSchemaValidationError(err error) error {
226+
var validationErr *schemavalidation.ValidationError
227+
if !errors.As(err, &validationErr) {
228+
return err
229+
}
230+
var errs []error
231+
for _, cause := range validationErr.Causes {
232+
if cause == nil || cause.ErrorKind == nil {
233+
continue
234+
}
235+
236+
var errMsg string
237+
switch e := cause.ErrorKind.(type) {
238+
case *kind.Format:
239+
errMsg = e.Err.Error()
240+
default:
241+
errMsg = cause.Error()
242+
}
243+
244+
instanceLocation := "." + strings.Join(cause.InstanceLocation, ".")
245+
if instanceLocation == "." {
246+
errs = append(errs, fmt.Errorf("%v", errMsg))
247+
} else {
248+
errs = append(errs, fmt.Errorf("at path %q: %s", instanceLocation, errMsg))
249+
}
250+
}
251+
if len(errs) > 0 {
252+
return errors.Join(errs...)
253+
}
254+
return err
255+
}
256+
257+
// notOwnNamespaceFmt returns a dynamically generated format specifically for the case where
258+
// a bundle does not support own namespace installation but a watch namespace can be optionally given
259+
func notOwnNamespaceFmt(installNamespace string) *schemavalidation.Format {
260+
return &schemavalidation.Format{
261+
Name: notOwnNamespaceFormat,
262+
Validate: func(v any) error {
263+
if err := dnsFormat.Validate(v); err != nil {
264+
return err
265+
}
266+
if v == installNamespace {
267+
return fmt.Errorf("unsupported value %q, watchNamespace cannot be install namespace", v)
268+
}
269+
return nil
270+
},
271+
}
272+
}

0 commit comments

Comments
 (0)