mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:27:24 +08:00
This PR removes the `types.KindKubeService` objects that were deprecated in Teleport 11. Teleport 12 removed the double heartbeat and for Teleport 13 we can safely drop the support of the old hearbeat based on `types.Server` objects. Clients older than Teleport 12 connecting to a Teleport 13 server will fail since they cannot heartbeat using the old mechanism.
250 lines
8.3 KiB
Go
250 lines
8.3 KiB
Go
/*
|
|
Copyright 2020 Gravitational, Inc.
|
|
|
|
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 utils
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/exp/slices"
|
|
"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"
|
|
)
|
|
|
|
// 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 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))
|
|
}
|
|
|
|
// KubeServicesPresence fetches a list of registered kubernetes servers.
|
|
// It's a subset of services.Presence.
|
|
type KubeServicesPresence interface {
|
|
// GetKubernetesServers returns a list of registered kubernetes servers.
|
|
GetKubernetesServers(context.Context) ([]types.KubeServer, error)
|
|
}
|
|
|
|
// KubeClusterNames returns a sorted list of unique kubernetes cluster
|
|
// names registered in p.
|
|
//
|
|
// DELETE IN 11.0.0, replaced by ListKubeClustersWithFilters
|
|
func KubeClusterNames(ctx context.Context, p KubeServicesPresence) ([]string, error) {
|
|
kss, err := p.GetKubernetesServers(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return extractAndSortKubeClusterNames(kss), nil
|
|
}
|
|
|
|
func extractAndSortKubeClusterNames(kubeServers []types.KubeServer) []string {
|
|
kubeClusters := extractAndSortKubeClusters(kubeServers)
|
|
kubeClusterNames := make([]string, len(kubeClusters))
|
|
for i := range kubeClusters {
|
|
kubeClusterNames[i] = kubeClusters[i].GetName()
|
|
}
|
|
|
|
return kubeClusterNames
|
|
}
|
|
|
|
// KubeClusters returns a sorted list of unique kubernetes clusters
|
|
// registered in p.
|
|
//
|
|
// DELETE IN 11.0.0, replaced by ListKubeClustersWithFilters
|
|
func KubeClusters(ctx context.Context, p KubeServicesPresence) ([]types.KubeCluster, error) {
|
|
kubeServers, err := p.GetKubernetesServers(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return extractAndSortKubeClusters(kubeServers), nil
|
|
}
|
|
|
|
// ListKubeClusterWithFilters returns a sorted list of unique kubernetes clusters
|
|
// registered in p.
|
|
func ListKubeClustersWithFilters(ctx context.Context, p client.ListResourcesClient, req proto.ListResourcesRequest) ([]types.KubeCluster, error) {
|
|
req.ResourceType = types.KindKubeServer
|
|
var kss []types.KubeServer
|
|
|
|
resources, err := client.GetResourcesWithFilters(ctx, p, req)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
kss, err = types.ResourcesWithLabels(resources).AsKubeServers()
|
|
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)
|
|
}
|
|
|
|
// CheckOrSetKubeCluster validates kubeClusterName if it's set, or a sane
|
|
// default based on registered clusters.
|
|
//
|
|
// If no clusters are registered, a NotFound error is returned.
|
|
func CheckOrSetKubeCluster(ctx context.Context, p KubeServicesPresence, kubeClusterName, teleportClusterName string) (string, error) {
|
|
kubeClusterNames, err := KubeClusterNames(ctx, p)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
if kubeClusterName != "" {
|
|
if !slices.Contains(kubeClusterNames, kubeClusterName) {
|
|
return "", trace.BadParameter("kubernetes cluster %q is not registered in this teleport cluster; you can list registered kubernetes clusters using 'tsh kube ls'", kubeClusterName)
|
|
}
|
|
return kubeClusterName, nil
|
|
}
|
|
// Default is the cluster with a name matching the Teleport cluster
|
|
// name (for backwards-compatibility with pre-5.0 behavior) or the
|
|
// first name alphabetically.
|
|
if len(kubeClusterNames) == 0 {
|
|
return "", trace.NotFound("no kubernetes clusters registered")
|
|
}
|
|
if slices.Contains(kubeClusterNames, teleportClusterName) {
|
|
return teleportClusterName, nil
|
|
}
|
|
return kubeClusterNames[0], nil
|
|
}
|