Files
teleport/lib/kube/utils/utils.go
T
Hugo Shaka c20095e9d6 Align semver libs (#52795)
* Convert autoupdate version handling to coreos/go-semver

* get the right version in installer endpoint + get rid of x/mod/semver

* depguard x/mod/semver

* Add nolint rules for existing x/mod/semver usages

* Add depguard explanation
2025-03-19 18:03:35 +00:00

215 lines
7.0 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package utils
import (
"context"
"encoding/hex"
"errors"
"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/automaticupgrades/version"
)
// GetKubeClient returns instance of client to the kubernetes cluster
// using in-cluster configuration if available and falling back to
// configuration file under configPath otherwise
func GetKubeClient(configPath string) (client *kubernetes.Clientset, config *rest.Config, err error) {
// if path to kubeconfig was provided, init config from it
if configPath != "" {
config, err = clientcmd.BuildConfigFromFlags("", configPath)
if err != nil {
return nil, nil, trace.Wrap(err)
}
} else {
// otherwise attempt to init as if connecting from cluster
config, err = rest.InClusterConfig()
if err != nil {
return nil, nil, trace.Wrap(err)
}
}
client, err = kubernetes.NewForConfig(config)
if err != nil {
return nil, nil, trace.Wrap(err)
}
return client, config, nil
}
// Kubeconfig is a parsed kubeconfig file representation.
type Kubeconfig struct {
CurrentContext string
Contexts map[string]*rest.Config
}
// GetKubeConfig returns kubernetes configuration from configPath file or, by
// default reads in-cluster configuration. If allConfigEntries is set, the
// returned Kubeconfig will contain all contexts from the kubeconfig file;
// otherwise it only contains the current context.
//
// TODO(awly): unit test this
func GetKubeConfig(configPath string, allConfigEntries bool, clusterName string) (*Kubeconfig, error) {
switch {
case configPath != "" && clusterName == "":
loader := &clientcmd.ClientConfigLoadingRules{ExplicitPath: configPath}
cfg, err := loader.Load()
if err != nil {
return nil, trace.Wrap(err)
}
res := &Kubeconfig{
CurrentContext: cfg.CurrentContext,
Contexts: make(map[string]*rest.Config, len(cfg.Contexts)),
}
if !allConfigEntries {
// Only current-context is requested.
clientCfg, err := clientcmd.NewNonInteractiveClientConfig(*cfg, cfg.CurrentContext, &clientcmd.ConfigOverrides{}, nil).ClientConfig()
if err != nil {
return nil, trace.Wrap(err)
}
res.Contexts[cfg.CurrentContext] = clientCfg
return res, nil
}
// All contexts are requested.
for n := range cfg.Contexts {
clientCfg, err := clientcmd.NewNonInteractiveClientConfig(*cfg, n, &clientcmd.ConfigOverrides{}, nil).ClientConfig()
if err != nil {
return nil, trace.Wrap(err)
}
res.Contexts[n] = clientCfg
}
return res, nil
case configPath == "" && clusterName != "":
cfg, err := rest.InClusterConfig()
if err != nil {
if errors.Is(err, rest.ErrNotInCluster) {
return nil, trace.NotFound("not running inside of a Kubernetes pod")
}
return nil, trace.Wrap(err)
}
return &Kubeconfig{
CurrentContext: clusterName,
Contexts: map[string]*rest.Config{clusterName: cfg},
}, nil
case configPath == "" && clusterName == "":
return nil, trace.NotFound("neither kubeconfig nor cluster name provided")
case configPath != "" && clusterName != "":
return nil, trace.BadParameter("only one of configPath or clusterName can be specified")
}
panic("unreachable")
}
// EncodeClusterName encodes cluster name for SNI matching
//
// For example:
//
// * Main cluster is main.example.com
// * Remote cluster is remote.example.com
//
// After 'tsh login' the URL of the Kubernetes endpoint of 'remote.example.com'
// when accessed 'via main.example.com' looks like this:
//
// 'k72656d6f74652e6578616d706c652e636f6d0a.main.example.com'
//
// For this to work, users have to add this address in public_addr section of kubernetes service
// to include 'main.example.com' in X509 '*.main.example.com' domain name
//
// where part '72656d6f74652e6578616d706c652e636f6d0a' is a hex encoded remote.example.com
//
// It is hex encoded to allow wildcard matching to work. In DNS wildcard match
// include only one '.'
func EncodeClusterName(clusterName string) string {
// k is to avoid first letter to be a number
return "k" + hex.EncodeToString([]byte(clusterName))
}
// ListKubeClustersWithFilters returns a sorted list of unique kubernetes clusters
// registered in p.
func ListKubeClustersWithFilters(ctx context.Context, p client.GetResourcesClient, req proto.ListResourcesRequest) ([]types.KubeCluster, error) {
req.ResourceType = types.KindKubeServer
kss, err := client.GetAllResources[types.KubeServer](ctx, p, &req)
if err != nil {
return nil, trace.Wrap(err)
}
return extractAndSortKubeClusters(kss), nil
}
func extractAndSortKubeClusters(kss []types.KubeServer) []types.KubeCluster {
uniqueClusters := make(map[string]types.KubeCluster)
for _, ks := range kss {
uniqueClusters[ks.GetName()] = ks.GetCluster()
}
kubeClusters := make([]types.KubeCluster, 0, len(uniqueClusters))
for _, cluster := range uniqueClusters {
kubeClusters = append(kubeClusters, cluster)
}
sorted := types.KubeClusters(kubeClusters)
sorted.SortByCustom(types.SortBy{
Field: types.ResourceMetadataName,
})
return []types.KubeCluster(sorted)
}
type Pinger interface {
Ping(context.Context) (proto.PingResponse, error)
}
// GetKubeAgentVersion returns a version of the Kube agent appropriate for this Teleport cluster. Used for example when deciding version
// for enrolling EKS clusters.
func GetKubeAgentVersion(ctx context.Context, pinger Pinger, clusterFeatures proto.Features, versionGetter version.Getter) (*semver.Version, error) {
pingResponse, err := pinger.Ping(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var agentVersion *semver.Version
// TODO(hugoShaka) remove the conditional check, we always use the cluster version
if clusterFeatures.GetAutomaticUpgrades() && clusterFeatures.GetCloud() {
defaultVersion, err := versionGetter.GetVersion(ctx)
if err == nil {
agentVersion = defaultVersion
} else if !errors.Is(err, &version.NoNewVersionError{}) {
return nil, trace.Wrap(err)
}
}
if agentVersion == nil {
clusterVersion, err := version.EnsureSemver(pingResponse.ServerVersion)
if err != nil {
return nil, trace.Wrap(err, "failed to parse cluster version")
} else {
agentVersion = clusterVersion
}
}
return agentVersion, nil
}