mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-03 16:38:35 +08:00
Adds the new linter and configures it to prevent direct error comparisions and type assertions in favor of errors.Is and errors.As. All current violations caught by the linter have been updated accordingly.
236 lines
7.9 KiB
Go
236 lines
7.9 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"
|
|
"slices"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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))
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// CheckKubeCluster validates kubeClusterName is registered with this Teleport cluster.
|
|
func CheckKubeCluster(ctx context.Context, p KubeServicesPresence, kubeClusterName string) error {
|
|
if kubeClusterName == "" {
|
|
return trace.BadParameter("kube cluster name should not be empty.")
|
|
}
|
|
kubeClusterNames, err := KubeClusterNames(ctx, p)
|
|
if err != nil {
|
|
return trace.Wrap(err, "failed to get list of available Kubernetes clusters.")
|
|
}
|
|
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 nil
|
|
}
|