From f5d0b3d849fb4ae26b653ea2e82b053d2a68696e Mon Sep 17 00:00:00 2001 From: Dakota Walsh <101994734+dakota-portainer@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:37:42 +1200 Subject: [PATCH] feat(kubernetes): Gateway api client included in kubeclient [C9S-244] (#2884) --- api/internal/upgrade/upgrade_kubernetes.go | 2 +- api/kubernetes/cli/client.go | 58 +++++++++++++++++----- api/kubernetes/snapshot.go | 2 +- go.mod | 5 +- go.sum | 14 +++--- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/api/internal/upgrade/upgrade_kubernetes.go b/api/internal/upgrade/upgrade_kubernetes.go index 9206c23104..d4a49a41c3 100644 --- a/api/internal/upgrade/upgrade_kubernetes.go +++ b/api/internal/upgrade/upgrade_kubernetes.go @@ -18,7 +18,7 @@ import ( func (service *service) upgradeKubernetes(environment *portainer.Endpoint, licenseKey, version string) error { ctx := context.TODO() - kubeCLI, err := service.kubernetesClientFactory.CreateClient(environment) + kubeCLI, _, err := service.kubernetesClientFactory.CreateClient(environment) if err != nil { return errors.WithMessage(err, "failed to get kubernetes client") } diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index b7fe999b66..5e6c15a7b1 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -20,6 +20,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" metricsv "k8s.io/metrics/pkg/client/clientset/versioned" + gatewaycliv1 "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/typed/apis/v1" ) const ( @@ -42,6 +43,7 @@ type ( // KubeClient represent a service used to execute Kubernetes operations KubeClient struct { cli kubernetes.Interface + gatewayCLI gatewaycliv1.GatewayV1Interface instanceID string mu sync.Mutex isKubeAdmin bool @@ -105,8 +107,10 @@ func (factory *ClientFactory) GetAddrHTTPS() string { return factory.AddrHTTPS } -// GetPrivilegedKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found. -// If no client is registered, it will create a new client, register it, and returns it. +// GetPrivilegedKubeClient checks if an existing client is already registered +// for the environment(endpoint) and returns it if one is found. +// +// If no client is registered, it will create a new client, register it, and return it. func (factory *ClientFactory) GetPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) { key := strconv.Itoa(int(endpoint.ID)) pcl, ok := factory.endpointProxyClients.Get(key) @@ -123,8 +127,11 @@ func (factory *ClientFactory) GetPrivilegedKubeClient(endpoint *portainer.Endpoi return kcl, nil } -// GetPrivilegedUserKubeClient checks if an existing admin client is already registered for the environment(endpoint) and user and returns it if one is found. -// If no client is registered, it will create a new client, register it, and returns it. +// GetPrivilegedUserKubeClient checks if an existing admin client is already +// registered for the environment(endpoint) and user and returns it if one is +// found. +// +// If no client is registered, it will create a new client, register it, and return it. func (factory *ClientFactory) GetPrivilegedUserKubeClient(endpoint *portainer.Endpoint, userID portainer.UserID) (*KubeClient, error) { key := strconv.Itoa(int(endpoint.ID)) + ".admin." + strconv.Itoa(int(userID)) pcl, ok := factory.endpointProxyClients.Get(key) @@ -174,13 +181,24 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k clientConfig.QPS = defaultKubeClientQPS clientConfig.Burst = defaultKubeClientBurst - cli, err := kubernetes.NewForConfig(clientConfig) + httpClient, err := rest.HTTPClientFor(clientConfig) if err != nil { - return nil, fmt.Errorf("failed to create a new clientset for the given config: %w", err) + return nil, fmt.Errorf("failed to create http client for the given config: %w", err) + } + + cli, err := kubernetes.NewForConfigAndClient(clientConfig, httpClient) + if err != nil { + return nil, fmt.Errorf("failed to create clientset for the given config: %w", err) + } + + gatewayCLI, err := gatewaycliv1.NewForConfigAndClient(clientConfig, httpClient) + if err != nil { + return nil, fmt.Errorf("failed to create gateway clientset for the given config: %w", err) } return &KubeClient{ cli: cli, + gatewayCLI: gatewayCLI, instanceID: factory.instanceID, isKubeAdmin: IsKubeAdmin, nonAdminNamespaces: NonAdminNamespaces, @@ -188,29 +206,45 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k } func (factory *ClientFactory) createCachedPrivilegedKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) { - cli, err := factory.CreateClient(endpoint) + cli, gatewayCLI, err := factory.CreateClient(endpoint) if err != nil { return nil, err } return &KubeClient{ cli: cli, + gatewayCLI: gatewayCLI, instanceID: factory.instanceID, isKubeAdmin: true, }, nil } -// CreateClient returns a pointer to a new Clientset instance. -func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { +// CreateClient returns a pointer to a new Kubernetes Core Clientset instance. +func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, *gatewaycliv1.GatewayV1Client, error) { switch endpoint.Type { case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment: c, err := factory.CreateConfig(endpoint) if err != nil { - return nil, err + return nil, nil, err } - return kubernetes.NewForConfig(c) + + httpClient, err := rest.HTTPClientFor(c) + if err != nil { + return nil, nil, fmt.Errorf("failed to create http client for the given config: %w", err) + } + + cli, err := kubernetes.NewForConfigAndClient(c, httpClient) + if err != nil { + return nil, nil, fmt.Errorf("failed to create clientset for the given config: %w", err) + } + + gatewayCLI, err := gatewaycliv1.NewForConfigAndClient(c, httpClient) + if err != nil { + return nil, nil, fmt.Errorf("failed to create gateway clientset for the given config: %w", err) + } + return cli, gatewayCLI, nil } - return nil, errors.New("unsupported environment type") + return nil, nil, errors.New("unsupported environment type") } // CreateConfig returns a pointer to a new kubeconfig ready to create a client. diff --git a/api/kubernetes/snapshot.go b/api/kubernetes/snapshot.go index fe3f505544..c8f6c2e598 100644 --- a/api/kubernetes/snapshot.go +++ b/api/kubernetes/snapshot.go @@ -19,7 +19,7 @@ func NewSnapshotter(clientFactory *cli.ClientFactory) *Snapshotter { // CreateSnapshot creates a snapshot of a specific Kubernetes environment(endpoint) func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.KubernetesSnapshot, error) { - client, err := snapshotter.clientFactory.CreateClient(endpoint) + client, _, err := snapshotter.clientFactory.CreateClient(endpoint) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 0ddfd95aed..ede03b7f34 100644 --- a/go.mod +++ b/go.mod @@ -72,6 +72,7 @@ require ( k8s.io/kubectl v0.35.1 k8s.io/metrics v0.35.1 oras.land/oras-go/v2 v2.6.0 + sigs.k8s.io/gateway-api v1.5.1 ) require ( @@ -364,13 +365,13 @@ require ( k8s.io/component-helpers v0.35.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/utils v0.0.0-20260108192941-914a6e750570 // indirect sigs.k8s.io/controller-runtime v0.23.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect tags.cncf.io/container-device-interface v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index dbd1b59ce4..ec0abb092d 100644 --- a/go.sum +++ b/go.sum @@ -757,8 +757,8 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= -github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.148.0 h1:CiTjQE/Hh5xK2t56ogrDK4nl0+tJPNmASCs4zEYZ/xU= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.148.0/go.mod h1:WUFkzTiOpt7EYyL67gv1GOf3RD8qKWGtin3lY9LYzW4= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.148.0 h1:1TLg6YrS3Au6F7xw3ws2Njbwj13IMqPplvGFi+18fWs= @@ -1268,12 +1268,14 @@ k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg= k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo= k8s.io/metrics v0.35.1 h1:MUcrUcWlq81XiripkydzCGsY9zQawDXfP9IICNNcVVw= k8s.io/metrics v0.35.1/go.mod h1:9x7xWOAOiWzHA0vaqLgSE4PXF3vyT5ts5XIbx8OSjiI= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= +k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/gateway-api v1.5.1 h1:RqVRIlkhLhUO8wOHKTLnTJA6o/1un4po4/6M1nRzdd0= +sigs.k8s.io/gateway-api v1.5.1/go.mod h1:GvCETiaMAlLym5CovLxGjS0NysqFk3+Yuq3/rh6QL2o= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= @@ -1282,8 +1284,8 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7 sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY=