diff --git a/cmd/clusterctl/cmd/move.go b/cmd/clusterctl/cmd/move.go index 2a51bf2c0d9b..83b4abac39dd 100644 --- a/cmd/clusterctl/cmd/move.go +++ b/cmd/clusterctl/cmd/move.go @@ -18,11 +18,17 @@ package cmd import ( "context" + "fmt" "github.com/pkg/errors" "github.com/spf13/cobra" + "k8s.io/client-go/rest" "sigs.k8s.io/cluster-api/cmd/clusterctl/client" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" + "sigs.k8s.io/cluster-api/util/apiwarnings" ) type moveOptions struct { @@ -34,6 +40,7 @@ type moveOptions struct { fromDirectory string toDirectory string dryRun bool + hideAPIWarnings string } var mo = &moveOptions{} @@ -80,6 +87,8 @@ func init() { "Write Cluster API objects and all dependencies from a management cluster to directory.") moveCmd.Flags().StringVar(&mo.fromDirectory, "from-directory", "", "Read Cluster API objects and all dependencies from a directory into a management cluster.") + moveCmd.Flags().StringVar(&mo.hideAPIWarnings, "hide-api-warnings", "default", + "Set of API server warnings to hide. Valid sets are \"default\" (includes metadata.finalizer warnings), \"all\" , and \"none\".") moveCmd.MarkFlagsMutuallyExclusive("to-directory", "to-kubeconfig") moveCmd.MarkFlagsMutuallyExclusive("from-directory", "to-directory") @@ -98,7 +107,55 @@ func runMove() error { return errors.New("please specify a target cluster using the --to-kubeconfig flag when not using --dry-run, --to-directory or --from-directory") } - c, err := client.New(ctx, cfgFile) + configClient, err := config.New(ctx, cfgFile) + if err != nil { + return err + } + + clientOptions := []client.Option{} + + var warningHandler rest.WarningHandler + switch mo.hideAPIWarnings { + case "all": + // Hide all warnings. + warningHandler = apiwarnings.DiscardAllHandler + case "default": + // Hide only the default set of warnings. + warningHandler = apiwarnings.DefaultHandler(logf.Log.WithName("API Server Warning")) + case "none": + // Hide no warnings. + warningHandler = apiwarnings.LogAllHandler(logf.Log.WithName("API Server Warning")) + default: + return fmt.Errorf( + "set of API warnings %q is unknown; choose \"default\", \"all\", or \"none\"", + mo.hideAPIWarnings, + ) + } + + if warningHandler != nil { + clientOptions = append(clientOptions, + client.InjectClusterClientFactory( + func(input client.ClusterClientFactoryInput) (cluster.Client, error) { + return cluster.New( + cluster.Kubeconfig(input.Kubeconfig), + configClient, + cluster.InjectYamlProcessor(input.Processor), + cluster.InjectProxy( + cluster.NewProxy( + cluster.Kubeconfig(input.Kubeconfig), + cluster.InjectWarningHandler( + warningHandler, + ), + )), + ), nil + }, + ), + // Ensure that the same configClient used by both the client constructor, and the cluster client factory. + client.InjectConfig(configClient), + ) + } + + c, err := client.New(ctx, cfgFile, clientOptions...) if err != nil { return err } diff --git a/util/apiwarnings/expressions.go b/util/apiwarnings/expressions.go new file mode 100644 index 000000000000..d97b44f8af30 --- /dev/null +++ b/util/apiwarnings/expressions.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apiwarnings + +import ( + "fmt" + "regexp" +) + +// DomainQualifiedFinalizerWarning is a regular expression that matches a +// warning that the API server returns when a finalizer is not domain-qualified. +func DomainQualifiedFinalizerWarning(domain string) regexp.Regexp { + return *regexp.MustCompile( + fmt.Sprintf("^metadata.finalizers:.*%s.*prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers$", domain), + ) +} diff --git a/util/apiwarnings/handler.go b/util/apiwarnings/handler.go index 3e74ea5c05c5..f9664bfcd33d 100644 --- a/util/apiwarnings/handler.go +++ b/util/apiwarnings/handler.go @@ -20,6 +20,9 @@ import ( "regexp" "github.com/go-logr/logr" + "k8s.io/client-go/rest" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) // DiscardMatchingHandler is a handler that discards API server warnings @@ -49,3 +52,23 @@ func (h *DiscardMatchingHandler) HandleWarningHeader(code int, _, message string h.Logger.Info(message) } + +// DefaultHandler is a handler that discards warnings that are the result of +// decisions made by the Cluster API project, but cannot be immediately +// addressed, and are therefore not helpful to the user. +func DefaultHandler(l logr.Logger) *DiscardMatchingHandler { + return &DiscardMatchingHandler{ + Logger: l, + Expressions: []regexp.Regexp{ + DomainQualifiedFinalizerWarning(clusterv1.GroupVersion.Group), + }, + } +} + +// DiscardAllHandler is a handler that discards all warnings. +var DiscardAllHandler = rest.NoWarnings{} + +// LogAllHandler is a handler that logs all warnings. +func LogAllHandler(l logr.Logger) rest.WarningHandler { + return &DiscardMatchingHandler{Logger: l} +} diff --git a/util/apiwarnings/handler_test.go b/util/apiwarnings/handler_test.go index f2d6abd6ebbc..0dfcf4e482b1 100644 --- a/util/apiwarnings/handler_test.go +++ b/util/apiwarnings/handler_test.go @@ -95,3 +95,83 @@ func TestDiscardMatchingHandler_uninitialized(t *testing.T) { h.HandleWarningHeader(299, "", "example") }).ToNot(Panic()) } + +func TestDefaultHandler(t *testing.T) { + tests := []struct { + name string + code int + message string + wantLogged bool + }{ + { + name: "log, if warning does not match any expression", + code: 299, + message: `metadata.finalizers: "foo.example.com": prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers`, + wantLogged: true, + }, + { + name: "do not log, if warning matches at least one expression", + code: 299, + message: `metadata.finalizers: "dockermachine.infrastructure.cluster.x-k8s.io": prefer a domain-qualified finalizer name to avoid accidental conflicts with other finalizer writers`, + wantLogged: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + logged := false + h := DefaultHandler( + funcr.New(func(_, _ string) { + logged = true + }, + funcr.Options{}, + ), + ) + h.HandleWarningHeader(tt.code, "", tt.message) + g.Expect(logged).To(Equal(tt.wantLogged)) + }) + } +} + +func TestLogAllHandler(t *testing.T) { + tests := []struct { + name string + code int + message string + wantLogged bool + }{ + { + name: "log, if code is 299, and message is not empty", + code: 299, + message: "warning", + wantLogged: true, + }, + { + name: "do not log, if code is not 299", + code: 0, + message: "warning", + wantLogged: false, + }, + { + name: "do not log, if message is empty", + code: 299, + message: "", + wantLogged: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + logged := false + h := LogAllHandler( + funcr.New(func(_, _ string) { + logged = true + }, + funcr.Options{}, + ), + ) + h.HandleWarningHeader(tt.code, "", tt.message) + g.Expect(logged).To(Equal(tt.wantLogged)) + }) + } +}