diff --git a/multicluster/clusterregistry_client.go b/multicluster/clusterregistry_client.go index f2a1ccdb..c5748ce4 100644 --- a/multicluster/clusterregistry_client.go +++ b/multicluster/clusterregistry_client.go @@ -49,29 +49,58 @@ var ErrDoesNotHaveToken = errors.New("secret does not have data.token") // https://github.com/kubernetes-retired/cluster-registry/blob/master/pkg/apis/clusterregistry/v1alpha1/types.go type ClusterRegistryClient struct { dynamic.Interface + + insecure bool + // proxy host for accessing cluster + clusterProxyHost string + // proxy host for accessing cluster, support {name} placeholder with the actual cluster name + clusterProxyPath string } var _ Interface = &ClusterRegistryClient{} // NewClusterRegistryClient initiates a ClusterRegistryClient -func NewClusterRegistryClient(config *rest.Config) (Interface, error) { +func NewClusterRegistryClient(config *rest.Config, options ...ClusterRegistryClientOption) (Interface, error) { dyn, err := dynamic.NewForConfig(config) if err != nil { return nil, err } - return &ClusterRegistryClient{Interface: dyn}, nil + registryClient := &ClusterRegistryClient{Interface: dyn} + for _, option := range options { + option(registryClient) + } + + return registryClient, nil } // NewClusterRegistryClientOrDie initiates a ClusterRegistryClient and // panics if it fails -func NewClusterRegistryClientOrDie(config *rest.Config) Interface { - clt, err := NewClusterRegistryClient(config) +func NewClusterRegistryClientOrDie(config *rest.Config, options ...ClusterRegistryClientOption) Interface { + clt, err := NewClusterRegistryClient(config, options...) if err != nil { panic(err) } return clt } +// ClusterRegistryClientOption functions for configuring a ClusterRegistryClient +type ClusterRegistryClientOption func(*ClusterRegistryClient) + +// ClusterProxyOption sets the proxy host and path for the cluster registry client +func ClusterProxyOption(proxyHost string, proxyPath string) ClusterRegistryClientOption { + return func(c *ClusterRegistryClient) { + c.clusterProxyHost = proxyHost + c.clusterProxyPath = proxyPath + } +} + +// ClusterProxyInsecure allows specifying whether the client should use an insecure connection. +func ClusterProxyInsecure(insecure bool) ClusterRegistryClientOption { + return func(c *ClusterRegistryClient) { + c.insecure = insecure + } +} + var ClusterRegistryGroupVersion = schema.GroupVersion{Group: "clusterregistry.k8s.io", Version: "v1alpha1"} var ClusterRegistryGVK = ClusterRegistryGroupVersion.WithKind("Cluster") var ClusterGVR = ClusterRegistryGroupVersion.WithResource("clusters") @@ -86,6 +115,14 @@ func (m *ClusterRegistryClient) GetConfig(ctx context.Context, clusterRef *corev return } config, err = m.GetConfigFromCluster(ctx, cluster) + if m.clusterProxyHost != "" { + proxyHost, err := ClusterProxyHost(m.clusterProxyHost, m.clusterProxyPath, cluster.GetName()) + if err != nil { + return nil, err + } + config.Host = proxyHost + } + return } @@ -161,7 +198,8 @@ func (m *ClusterRegistryClient) GetConfigFromCluster(ctx context.Context, cluste config = &rest.Config{ Host: address, TLSClientConfig: rest.TLSClientConfig{ - CAData: caBundle, + CAData: caBundle, + Insecure: m.insecure, }, } diff --git a/multicluster/clusterregistry_client_test.go b/multicluster/clusterregistry_client_test.go index 134d4837..034cffd8 100644 --- a/multicluster/clusterregistry_client_test.go +++ b/multicluster/clusterregistry_client_test.go @@ -60,6 +60,20 @@ func TestClusterRegistryClientGetConfig(t *testing.T) { g.Expect(config.BearerToken).To(Equal("abctoken")) }) + t.Run("get config with proxy", func(t *testing.T) { + g := NewGomegaWithT(t) + + opt := ClusterProxyOption("proxy.test", "kubernetes/{name}") + opt(clusterClient) + + config, err := clusterClient.GetConfig(ctx, ref) + + g.Expect(err).To(BeNil()) + g.Expect(config).ToNot(BeNil()) + g.Expect(config.Host).To(Equal("https://proxy.test/kubernetes/my-cluster")) + g.Expect(config.BearerToken).To(Equal("abctoken")) + }) + t.Run("get client", func(t *testing.T) { client, err := clusterClient.GetClient(ctx, ref, scheme.Scheme) diff --git a/multicluster/proxy.go b/multicluster/proxy.go new file mode 100644 index 00000000..95548767 --- /dev/null +++ b/multicluster/proxy.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 The Katanomi 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 multicluster + +import ( + "fmt" + "net/url" + "strings" +) + +const ( + // placeHolderClusterName defines the placeholder for the cluster name in proxy paths. + placeHolderClusterName = "{name}" +) + +// ClusterProxyHost constructs a complete proxy URL by replacing the cluster name placeholder in the proxy path +// It takes the proxy host and path, replaces the "{name}" placeholder with the actual cluster name, +// and returns the formatted proxy URL. +func ClusterProxyHost(proxyHost string, proxyPath string, clusterName string) (string, error) { + proxyPath = strings.ReplaceAll(proxyPath, placeHolderClusterName, clusterName) + proxyPath = strings.TrimPrefix(proxyPath, "/") + + hostURL, err := url.Parse(proxyHost) + if err != nil { + return "", err + } + //ensuring the host URL uses the HTTPS scheme if not specified. + if hostURL.Scheme == "" { + hostURL.Scheme = "https" + } + + return fmt.Sprintf("%s/%s", hostURL.String(), proxyPath), nil +} diff --git a/multicluster/proxy_test.go b/multicluster/proxy_test.go new file mode 100644 index 00000000..8f2ca563 --- /dev/null +++ b/multicluster/proxy_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2024 The Katanomi 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 multicluster + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestClusterProxyHost(t *testing.T) { + tests := map[string]struct { + host string + endpoint string + clusterName string + expected string + }{ + "host with scheme": { + host: "http://abc.test", + endpoint: "/kubernetes", + clusterName: "global", + expected: "http://abc.test/kubernetes", + }, + "endpoint without slash": { + host: "http://abc.test", + endpoint: "kubernetes", + clusterName: "global", + expected: "http://abc.test/kubernetes", + }, + "host with scheme and port": { + host: "https://abc.test:443", + endpoint: "/kubernetes", + clusterName: "global", + expected: "https://abc.test:443/kubernetes", + }, + "host without scheme": { + host: "abc.test", + endpoint: "/kubernetes", + clusterName: "global", + expected: "https://abc.test/kubernetes", + }, + "endpoint with name placeholder": { + host: "abc.test", + endpoint: "/kubernetes/{name}", + clusterName: "global", + expected: "https://abc.test/kubernetes/global", + }, + } + + for name, item := range tests { + t.Run(name, func(t *testing.T) { + g := NewGomegaWithT(t) + g.Expect(ClusterProxyHost(item.host, item.endpoint, item.clusterName)).To(Equal(item.expected)) + }) + } +} diff --git a/sharedmain/app.go b/sharedmain/app.go index a028753e..a86ccd5d 100644 --- a/sharedmain/app.go +++ b/sharedmain/app.go @@ -88,6 +88,9 @@ var ( WebServerPort int InsecureSkipVerify bool + + ClusterProxyHost string + ClusterProxyPath string ) // AppBuilder builds an app using multiple configuration options @@ -154,6 +157,10 @@ func ParseFlag() { "Command-line flags override configuration from this file.") flag.BoolVar(&InsecureSkipVerify, "insecure-skip-tls-verify", false, "skip TLS verification and disable cert checking (default: false)") + flag.StringVar(&ClusterProxyHost, "cluster-proxy-host", "", + "Specify the hostname or IP address of the cluster proxy.") + flag.StringVar(&ClusterProxyPath, "cluster-proxy-path", "", + "Specify the endpoint path for the cluster proxy, '{name}' as the placeholder for the cluster name.") flag.IntVar(&WebServerPort, "web-server-port", 8100, "http web server port") flag.Parse() } @@ -192,7 +199,11 @@ func (a *AppBuilder) init() { return a.ConfigMapWatcher.Start(ctx.Done()) }) - a.Context = multicluster.WithMultiCluster(a.Context, multicluster.NewClusterRegistryClientOrDie(a.Config)) + multiCluster := multicluster.NewClusterRegistryClientOrDie(a.Config, + multicluster.ClusterProxyOption(ClusterProxyHost, ClusterProxyPath), + multicluster.ClusterProxyInsecure(InsecureSkipVerify), + ) + a.Context = multicluster.WithMultiCluster(a.Context, multiCluster) a.container = restful.NewContainer() a.container.Router(restful.RouterJSR311{})