| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- package kubernetes
- import (
- "context"
- "errors"
- "fmt"
- "path/filepath"
- "regexp"
- "strings"
- "time"
- "github.com/porter-dev/porter/internal/models"
- "github.com/porter-dev/porter/internal/oauth"
- "github.com/porter-dev/porter/internal/repository"
- "golang.org/x/oauth2"
- "k8s.io/apimachinery/pkg/api/meta"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/cli-runtime/pkg/genericclioptions"
- "k8s.io/client-go/discovery"
- diskcached "k8s.io/client-go/discovery/cached/disk"
- "k8s.io/client-go/dynamic"
- "k8s.io/client-go/kubernetes"
- "k8s.io/client-go/kubernetes/fake"
- "k8s.io/client-go/rest"
- "k8s.io/client-go/restmapper"
- "k8s.io/client-go/tools/clientcmd"
- "k8s.io/client-go/tools/clientcmd/api"
- "k8s.io/client-go/util/homedir"
- ints "github.com/porter-dev/porter/internal/models/integrations"
- // this line will register plugins
- _ "k8s.io/client-go/plugin/pkg/client/auth"
- )
- // GetDynamicClientOutOfClusterConfig creates a new dynamic client using the OutOfClusterConfig
- func GetDynamicClientOutOfClusterConfig(conf *OutOfClusterConfig) (dynamic.Interface, error) {
- var restConf *rest.Config
- var err error
- if conf.AllowInClusterConnections && conf.Cluster.AuthMechanism == models.InCluster {
- restConf, err = rest.InClusterConfig()
- } else {
- restConf, err = conf.ToRESTConfig()
- }
- if err != nil {
- return nil, err
- }
- client, err := dynamic.NewForConfig(restConf)
- if err != nil {
- return nil, err
- }
- return client, nil
- }
- // GetAgentOutOfClusterConfig creates a new Agent using the OutOfClusterConfig
- func GetAgentOutOfClusterConfig(conf *OutOfClusterConfig) (*Agent, error) {
- if conf.AllowInClusterConnections && conf.Cluster.AuthMechanism == models.InCluster {
- return GetAgentInClusterConfig(conf.DefaultNamespace)
- }
- restConf, err := conf.ToRESTConfig()
- if err != nil {
- return nil, err
- }
- clientset, err := kubernetes.NewForConfig(restConf)
- if err != nil {
- return nil, err
- }
- return &Agent{conf, clientset}, nil
- }
- // IsInCluster returns true if the process is running in a Kubernetes cluster,
- // false otherwise
- func IsInCluster() bool {
- _, err := rest.InClusterConfig()
- // If the error is not nil, it is either rest.ErrNotInCluster or the in-cluster
- // config cannot be read. In either case, in-cluster operations are not supported.
- return err == nil
- }
- // GetAgentInClusterConfig uses the service account that kubernetes
- // gives to pods to connect
- func GetAgentInClusterConfig(namespace string) (*Agent, error) {
- conf, err := rest.InClusterConfig()
- if err != nil {
- return nil, err
- }
- restClientGetter := NewRESTClientGetterFromInClusterConfig(conf, namespace)
- clientset, err := kubernetes.NewForConfig(conf)
- return &Agent{restClientGetter, clientset}, nil
- }
- // GetAgentTesting creates a new Agent using an optional existing storage class
- func GetAgentTesting(objects ...runtime.Object) *Agent {
- return &Agent{&fakeRESTClientGetter{}, fake.NewSimpleClientset(objects...)}
- }
- // OutOfClusterConfig is the set of parameters required for an out-of-cluster connection.
- // This implements RESTClientGetter
- type OutOfClusterConfig struct {
- Cluster *models.Cluster
- Repo repository.Repository
- DefaultNamespace string // optional
- AllowInClusterConnections bool
- Timeout time.Duration // optional
- // Only required if using DigitalOcean OAuth as an auth mechanism
- DigitalOceanOAuth *oauth2.Config
- }
- // ToRESTConfig creates a kubernetes REST client factory -- it calls ClientConfig on
- // the result of ToRawKubeConfigLoader, and also adds a custom http transport layer
- // if necessary (required for GCP auth)
- func (conf *OutOfClusterConfig) ToRESTConfig() (*rest.Config, error) {
- cmdConf, err := conf.GetClientConfigFromCluster()
- if err != nil {
- return nil, err
- }
- restConf, err := cmdConf.ClientConfig()
- if err != nil {
- return nil, err
- }
- restConf.Timeout = conf.Timeout
- rest.SetKubernetesDefaults(restConf)
- return restConf, nil
- }
- // ToRawKubeConfigLoader creates a clientcmd.ClientConfig from the raw kubeconfig found in
- // the OutOfClusterConfig. It does not implement loading rules or overrides.
- func (conf *OutOfClusterConfig) ToRawKubeConfigLoader() clientcmd.ClientConfig {
- cmdConf, _ := conf.GetClientConfigFromCluster()
- return cmdConf
- }
- // ToDiscoveryClient returns a CachedDiscoveryInterface using a computed RESTConfig
- // It's required to implement the interface genericclioptions.RESTClientGetter
- func (conf *OutOfClusterConfig) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
- // From: k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go > func (*configFlags) ToDiscoveryClient()
- restConf, err := conf.ToRESTConfig()
- if err != nil {
- return nil, err
- }
- restConf.Burst = 100
- defaultHTTPCacheDir := filepath.Join(homedir.HomeDir(), ".kube", "http-cache")
- // takes the parentDir and the host and comes up with a "usually non-colliding" name for the discoveryCacheDir
- parentDir := filepath.Join(homedir.HomeDir(), ".kube", "cache", "discovery")
- // strip the optional scheme from host if its there:
- schemelessHost := strings.Replace(strings.Replace(restConf.Host, "https://", "", 1), "http://", "", 1)
- // now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived
- safeHost := regexp.MustCompile(`[^(\w/\.)]`).ReplaceAllString(schemelessHost, "_")
- discoveryCacheDir := filepath.Join(parentDir, safeHost)
- return diskcached.NewCachedDiscoveryClientForConfig(restConf, discoveryCacheDir, defaultHTTPCacheDir, time.Duration(10*time.Minute))
- }
- // ToRESTMapper returns a mapper
- func (conf *OutOfClusterConfig) ToRESTMapper() (meta.RESTMapper, error) {
- // From: k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go > func (*configFlags) ToRESTMapper()
- discoveryClient, err := conf.ToDiscoveryClient()
- if err != nil {
- return nil, err
- }
- mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
- expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
- return expander, nil
- }
- // GetClientConfigFromCluster will construct new clientcmd.ClientConfig using
- // the configuration saved within a Cluster model
- func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientConfig, error) {
- if conf.Cluster == nil {
- return nil, fmt.Errorf("cluster cannot be nil")
- }
- if conf.Cluster.AuthMechanism == models.Local {
- kubeAuth, err := conf.Repo.KubeIntegration().ReadKubeIntegration(
- conf.Cluster.ProjectID,
- conf.Cluster.KubeIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- return clientcmd.NewClientConfigFromBytes(kubeAuth.Kubeconfig)
- }
- apiConfig, err := conf.CreateRawConfigFromCluster()
- if err != nil {
- return nil, err
- }
- overrides := &clientcmd.ConfigOverrides{}
- if conf.DefaultNamespace != "" {
- overrides.Context = api.Context{
- Namespace: conf.DefaultNamespace,
- }
- }
- config := clientcmd.NewDefaultClientConfig(*apiConfig, overrides)
- return config, nil
- }
- func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error) {
- cluster := conf.Cluster
- ctx := context.Background()
- apiConfig := &api.Config{}
- clusterMap := make(map[string]*api.Cluster)
- clusterMap[cluster.Name] = &api.Cluster{
- Server: cluster.Server,
- LocationOfOrigin: cluster.ClusterLocationOfOrigin,
- TLSServerName: cluster.TLSServerName,
- InsecureSkipTLSVerify: cluster.InsecureSkipTLSVerify,
- CertificateAuthorityData: cluster.CertificateAuthorityData,
- }
- // construct the auth infos
- authInfoName := cluster.Name + "-" + string(cluster.AuthMechanism)
- authInfoMap := make(map[string]*api.AuthInfo)
- authInfoMap[authInfoName] = &api.AuthInfo{
- LocationOfOrigin: cluster.UserLocationOfOrigin,
- Impersonate: cluster.UserImpersonate,
- }
- if groups := strings.Split(cluster.UserImpersonateGroups, ","); len(groups) > 0 && groups[0] != "" {
- authInfoMap[authInfoName].ImpersonateGroups = groups
- }
- switch cluster.AuthMechanism {
- case models.X509:
- kubeAuth, err := conf.Repo.KubeIntegration().ReadKubeIntegration(
- cluster.ProjectID,
- cluster.KubeIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- authInfoMap[authInfoName].ClientCertificateData = kubeAuth.ClientCertificateData
- authInfoMap[authInfoName].ClientKeyData = kubeAuth.ClientKeyData
- case models.Basic:
- kubeAuth, err := conf.Repo.KubeIntegration().ReadKubeIntegration(
- cluster.ProjectID,
- cluster.KubeIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- authInfoMap[authInfoName].Username = string(kubeAuth.Username)
- authInfoMap[authInfoName].Password = string(kubeAuth.Password)
- case models.Bearer:
- kubeAuth, err := conf.Repo.KubeIntegration().ReadKubeIntegration(
- cluster.ProjectID,
- cluster.KubeIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- authInfoMap[authInfoName].Token = string(kubeAuth.Token)
- case models.OIDC:
- oidcAuth, err := conf.Repo.OIDCIntegration().ReadOIDCIntegration(
- cluster.ProjectID,
- cluster.OIDCIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- authInfoMap[authInfoName].AuthProvider = &api.AuthProviderConfig{
- Name: "oidc",
- Config: map[string]string{
- "idp-issuer-url": string(oidcAuth.IssuerURL),
- "client-id": string(oidcAuth.ClientID),
- "client-secret": string(oidcAuth.ClientSecret),
- "idp-certificate-authority-data": string(oidcAuth.CertificateAuthorityData),
- "id-token": string(oidcAuth.IDToken),
- "refresh-token": string(oidcAuth.RefreshToken),
- },
- }
- case models.GCP:
- gcpAuth, err := conf.Repo.GCPIntegration().ReadGCPIntegration(
- cluster.ProjectID,
- cluster.GCPIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- tok, err := gcpAuth.GetBearerToken(
- conf.getTokenCache,
- conf.setTokenCache,
- "https://www.googleapis.com/auth/cloud-platform",
- )
- if tok == nil && err != nil {
- return nil, err
- }
- // add this as a bearer token
- authInfoMap[authInfoName].Token = tok.AccessToken
- case models.AWS:
- awsAuth, err := conf.Repo.AWSIntegration().ReadAWSIntegration(
- cluster.ProjectID,
- cluster.AWSIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- awsClusterID := cluster.Name
- shouldOverride := false
- if cluster.AWSClusterID != "" {
- awsClusterID = cluster.AWSClusterID
- shouldOverride = true
- }
- tok, err := awsAuth.GetBearerToken(ctx, conf.getTokenCache, conf.setTokenCache, awsClusterID, shouldOverride)
- if err != nil {
- return nil, err
- }
- // add this as a bearer token
- authInfoMap[authInfoName].Token = tok
- case models.DO:
- oauthInt, err := conf.Repo.OAuthIntegration().ReadOAuthIntegration(
- cluster.ProjectID,
- cluster.DOIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- tok, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, conf.DigitalOceanOAuth, oauth.MakeUpdateOAuthIntegrationTokenFunction(oauthInt, conf.Repo))
- if err != nil {
- return nil, err
- }
- // add this as a bearer token
- authInfoMap[authInfoName].Token = tok
- case models.Azure:
- azInt, err := conf.Repo.AzureIntegration().ReadAzureIntegration(
- cluster.ProjectID,
- cluster.AzureIntegrationID,
- )
- if err != nil {
- return nil, err
- }
- authInfoMap[authInfoName].Token = string(azInt.AKSPassword)
- default:
- return nil, errors.New("not a supported auth mechanism")
- }
- // create a context of the cluster name
- contextMap := make(map[string]*api.Context)
- contextMap[cluster.Name] = &api.Context{
- LocationOfOrigin: cluster.ClusterLocationOfOrigin,
- Cluster: cluster.Name,
- AuthInfo: authInfoName,
- }
- apiConfig.Clusters = clusterMap
- apiConfig.AuthInfos = authInfoMap
- apiConfig.Contexts = contextMap
- apiConfig.CurrentContext = cluster.Name
- return apiConfig, nil
- }
- func (conf *OutOfClusterConfig) getTokenCache() (tok *ints.TokenCache, err error) {
- return &conf.Cluster.TokenCache.TokenCache, nil
- }
- func (conf *OutOfClusterConfig) setTokenCache(token string, expiry time.Time) error {
- _, err := conf.Repo.Cluster().UpdateClusterTokenCache(
- &ints.ClusterTokenCache{
- ClusterID: conf.Cluster.ID,
- TokenCache: ints.TokenCache{
- Token: []byte(token),
- Expiry: expiry,
- },
- },
- )
- return err
- }
- // NewRESTClientGetterFromInClusterConfig returns a RESTClientGetter using
- // default values set from the *rest.Config
- func NewRESTClientGetterFromInClusterConfig(conf *rest.Config, namespace string) genericclioptions.RESTClientGetter {
- cfs := genericclioptions.NewConfigFlags(false)
- if namespace != "" {
- cfs.Namespace = &namespace
- }
- cfs.ClusterName = &conf.ServerName
- cfs.Insecure = &conf.Insecure
- cfs.APIServer = &conf.Host
- cfs.CAFile = &conf.CAFile
- cfs.KeyFile = &conf.KeyFile
- cfs.CertFile = &conf.CertFile
- cfs.BearerToken = &conf.BearerToken
- cfs.Timeout = stringptr(conf.Timeout.String())
- cfs.Impersonate = &conf.Impersonate.UserName
- cfs.ImpersonateGroup = &conf.Impersonate.Groups
- cfs.Username = &conf.Username
- cfs.Password = &conf.Password
- return cfs
- }
- func stringptr(val string) *string {
- return &val
- }
- type fakeRESTClientGetter struct{}
- func (f *fakeRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
- return nil, nil
- }
- func (f *fakeRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
- return nil
- }
- func (f *fakeRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
- return nil, nil
- }
- func (f *fakeRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
- return nil, nil
- }
|