| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593 |
- package kubernetes
- import (
- "context"
- "errors"
- "strings"
- "github.com/porter-dev/porter/internal/models"
- "golang.org/x/oauth2/google"
- "k8s.io/client-go/tools/clientcmd"
- "k8s.io/client-go/tools/clientcmd/api"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/credentials"
- "github.com/aws/aws-sdk-go/aws/session"
- token "sigs.k8s.io/aws-iam-authenticator/pkg/token"
- )
- // GetServiceAccountCandidates parses a kubeconfig for a list of service account
- // candidates.
- func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCandidate, error) {
- config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
- if err != nil {
- return nil, err
- }
- rawConf, err := config.RawConfig()
- if err != nil {
- return nil, err
- }
- res := make([]*models.ServiceAccountCandidate, 0)
- for contextName, context := range rawConf.Contexts {
- clusterName := context.Cluster
- awsClusterID := ""
- authInfoName := context.AuthInfo
- // get the auth mechanism and actions
- authMechanism, authInfoActions := parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
- clusterActions := parseClusterForActions(rawConf.Clusters[clusterName])
- actions := append(authInfoActions, clusterActions...)
- // if auth mechanism is unsupported, we'll skip it
- if authMechanism == models.NotAvailable {
- continue
- } else if authMechanism == models.AWS {
- // if the auth mechanism is AWS, we need to parse more explicitly
- // for the cluster id
- awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
- }
- // construct the raw kubeconfig that's relevant for that context
- contextConf, err := getConfigForContext(&rawConf, contextName)
- if err != nil {
- continue
- }
- rawBytes, err := clientcmd.Write(*contextConf)
- if err == nil {
- // create the candidate service account
- res = append(res, &models.ServiceAccountCandidate{
- Actions: actions,
- Kind: "connector",
- ClusterName: clusterName,
- ClusterEndpoint: rawConf.Clusters[clusterName].Server,
- AuthMechanism: authMechanism,
- AWSClusterIDGuess: awsClusterID,
- Kubeconfig: rawBytes,
- })
- }
- }
- return res, nil
- }
- // GetRawConfigFromBytes returns the clientcmdapi.Config from kubeconfig
- // bytes
- func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
- config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
- if err != nil {
- return nil, err
- }
- rawConf, err := config.RawConfig()
- if err != nil {
- return nil, err
- }
- return &rawConf, nil
- }
- // Parsing rules are:
- //
- // (1) If a client certificate + client key exist, uses x509 auth mechanism
- // (2) If an oidc/gcp/aws plugin exists, uses that auth mechanism
- // (3) If a bearer token exists, uses bearer token auth mechanism
- // (4) If a username/password exist, uses basic auth mechanism
- // (5) Otherwise, the config gets skipped
- //
- func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, actions []models.ServiceAccountAction) {
- actions = make([]models.ServiceAccountAction, 0)
- if (authInfo.ClientCertificate != "" || len(authInfo.ClientCertificateData) != 0) &&
- (authInfo.ClientKey != "" || len(authInfo.ClientKeyData) != 0) {
- if len(authInfo.ClientCertificateData) == 0 {
- actions = append(actions, models.ServiceAccountAction{
- Name: models.ClientCertDataAction,
- Resolved: false,
- Filename: authInfo.ClientCertificate,
- })
- }
- if len(authInfo.ClientKeyData) == 0 {
- actions = append(actions, models.ServiceAccountAction{
- Name: models.ClientKeyDataAction,
- Resolved: false,
- Filename: authInfo.ClientKey,
- })
- }
- return models.X509, actions
- }
- if authInfo.AuthProvider != nil {
- switch authInfo.AuthProvider.Name {
- case "oidc":
- filename, isFile := authInfo.AuthProvider.Config["idp-certificate-authority"]
- data, isData := authInfo.AuthProvider.Config["idp-certificate-authority-data"]
- if isFile && (!isData || data == "") {
- return models.OIDC, []models.ServiceAccountAction{
- models.ServiceAccountAction{
- Name: models.OIDCIssuerDataAction,
- Resolved: false,
- Filename: filename,
- },
- }
- }
- return models.OIDC, actions
- case "gcp":
- return models.GCP, []models.ServiceAccountAction{
- models.ServiceAccountAction{
- Name: models.GCPKeyDataAction,
- Resolved: false,
- },
- }
- }
- }
- if authInfo.Exec != nil {
- if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
- return models.AWS, []models.ServiceAccountAction{
- models.ServiceAccountAction{
- Name: models.AWSDataAction,
- Resolved: false,
- },
- }
- }
- }
- if authInfo.Token != "" || authInfo.TokenFile != "" {
- if authInfo.Token == "" {
- return models.Bearer, []models.ServiceAccountAction{
- models.ServiceAccountAction{
- Name: models.TokenDataAction,
- Resolved: false,
- Filename: authInfo.TokenFile,
- },
- }
- }
- return models.Bearer, actions
- }
- if authInfo.Username != "" && authInfo.Password != "" {
- return models.Basic, actions
- }
- return models.NotAvailable, actions
- }
- // Parses the cluster object to determine actions -- only currently supported action is
- // population of the cluster certificate authority data
- func parseClusterForActions(cluster *api.Cluster) (actions []models.ServiceAccountAction) {
- actions = make([]models.ServiceAccountAction, 0)
- if cluster.CertificateAuthority != "" && len(cluster.CertificateAuthorityData) == 0 {
- return []models.ServiceAccountAction{
- models.ServiceAccountAction{
- Name: models.ClusterCADataAction,
- Resolved: false,
- Filename: cluster.CertificateAuthority,
- },
- }
- }
- return actions
- }
- func parseAuthInfoForAWSClusterID(authInfo *api.AuthInfo, fallback string) string {
- if authInfo.Exec != nil {
- if authInfo.Exec.Command == "aws" {
- // look for --cluster-name flag
- for i, arg := range authInfo.Exec.Args {
- if arg == "--cluster-name" && len(authInfo.Exec.Args) > i+1 {
- return authInfo.Exec.Args[i+1]
- }
- }
- } else if authInfo.Exec.Command == "aws-iam-authenticator" {
- // look for -i or --cluster-id flag
- for i, arg := range authInfo.Exec.Args {
- if (arg == "-i" || arg == "--cluster-id") && len(authInfo.Exec.Args) > i+1 {
- return authInfo.Exec.Args[i+1]
- }
- }
- }
- }
- return fallback
- }
- // getKubeconfigForContext returns the raw kubeconfig associated with only a
- // single context of the raw config
- func getConfigForContext(
- rawConf *api.Config,
- contextName string,
- ) (*api.Config, error) {
- copyConf := rawConf.DeepCopy()
- copyConf.Clusters = make(map[string]*api.Cluster)
- copyConf.AuthInfos = make(map[string]*api.AuthInfo)
- copyConf.Contexts = make(map[string]*api.Context)
- copyConf.CurrentContext = contextName
- context, ok := rawConf.Contexts[contextName]
- if ok {
- userName := context.AuthInfo
- clusterName := context.Cluster
- authInfo, userFound := rawConf.AuthInfos[userName]
- cluster, clusterFound := rawConf.Clusters[clusterName]
- if userFound && clusterFound {
- copyConf.Clusters[clusterName] = cluster
- copyConf.AuthInfos[userName] = authInfo
- copyConf.Contexts[contextName] = context
- } else {
- return nil, errors.New("linked user and cluster not found")
- }
- } else {
- return nil, errors.New("context not found")
- }
- return copyConf, nil
- }
- // GetClientConfigFromServiceAccount will construct new clientcmd.ClientConfig using
- // the configuration saved within a ServiceAccount model
- func GetClientConfigFromServiceAccount(
- sa *models.ServiceAccount,
- clusterID uint,
- updateTokenCache UpdateTokenCacheFunc,
- ) (clientcmd.ClientConfig, error) {
- apiConfig, err := createRawConfigFromServiceAccount(sa, clusterID, updateTokenCache)
- if err != nil {
- return nil, err
- }
- config := clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{})
- return config, nil
- }
- func createRawConfigFromServiceAccount(
- sa *models.ServiceAccount,
- clusterID uint,
- updateTokenCache UpdateTokenCacheFunc,
- ) (*api.Config, error) {
- apiConfig := &api.Config{}
- var cluster *models.Cluster = nil
- // find the cluster within the ServiceAccount configuration
- for _, _cluster := range sa.Clusters {
- if _cluster.ID == clusterID {
- cluster = &_cluster
- }
- }
- if cluster == nil {
- return nil, errors.New("cluster not found")
- }
- clusterMap := make(map[string]*api.Cluster)
- clusterMap[cluster.Name] = &api.Cluster{
- LocationOfOrigin: cluster.LocationOfOrigin,
- Server: cluster.Server,
- TLSServerName: cluster.TLSServerName,
- InsecureSkipTLSVerify: cluster.InsecureSkipTLSVerify,
- CertificateAuthorityData: cluster.CertificateAuthorityData,
- }
- // construct the auth infos
- authInfoName := cluster.Name + "-" + sa.AuthMechanism
- authInfoMap := make(map[string]*api.AuthInfo)
- authInfoMap[authInfoName] = &api.AuthInfo{
- LocationOfOrigin: sa.LocationOfOrigin,
- Impersonate: sa.Impersonate,
- }
- if groups := strings.Split(sa.ImpersonateGroups, ","); len(groups) > 0 && groups[0] != "" {
- authInfoMap[authInfoName].ImpersonateGroups = groups
- }
- switch sa.AuthMechanism {
- case models.X509:
- authInfoMap[authInfoName].ClientCertificateData = sa.ClientCertificateData
- authInfoMap[authInfoName].ClientKeyData = sa.ClientKeyData
- case models.Basic:
- authInfoMap[authInfoName].Username = sa.Username
- authInfoMap[authInfoName].Password = sa.Password
- case models.Bearer:
- authInfoMap[authInfoName].Token = sa.Token
- case models.OIDC:
- authInfoMap[authInfoName].AuthProvider = &api.AuthProviderConfig{
- Name: "oidc",
- Config: map[string]string{
- "idp-issuer-url": sa.OIDCIssuerURL,
- "client-id": sa.OIDCClientID,
- "client-secret": sa.OIDCClientSecret,
- "idp-certificate-authority-data": sa.OIDCCertificateAuthorityData,
- "id-token": sa.OIDCIDToken,
- "refresh-token": sa.OIDCRefreshToken,
- },
- }
- case models.GCP:
- tok, err := getGCPToken(sa, updateTokenCache)
- if err != nil {
- return nil, err
- }
- // add this as a bearer token
- authInfoMap[authInfoName].Token = tok
- case models.AWS:
- tok, err := getAWSToken(sa, updateTokenCache)
- if err != nil {
- return nil, err
- }
- // add this as a bearer token
- authInfoMap[authInfoName].Token = tok
- 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.LocationOfOrigin,
- Cluster: cluster.Name,
- AuthInfo: authInfoName,
- }
- apiConfig.Clusters = clusterMap
- apiConfig.AuthInfos = authInfoMap
- apiConfig.Contexts = contextMap
- apiConfig.CurrentContext = cluster.Name
- return apiConfig, nil
- }
- func getGCPToken(
- sa *models.ServiceAccount,
- updateTokenCache UpdateTokenCacheFunc,
- ) (string, error) {
- // check the token cache for a non-expired token
- if tok := sa.TokenCache.Token; !sa.TokenCache.IsExpired() && tok != "" {
- return tok, nil
- }
- creds, err := google.CredentialsFromJSON(
- context.Background(),
- sa.GCPKeyData,
- "https://www.googleapis.com/auth/cloud-platform",
- )
- if err != nil {
- return "", err
- }
- tok, err := creds.TokenSource.Token()
- if err != nil {
- return "", err
- }
- // update the token cache
- updateTokenCache(tok.AccessToken, tok.Expiry)
- return tok.AccessToken, nil
- }
- func getAWSToken(
- sa *models.ServiceAccount,
- updateTokenCache UpdateTokenCacheFunc,
- ) (string, error) {
- // check the token cache for a non-expired token
- if tok := sa.TokenCache.Token; !sa.TokenCache.IsExpired() && tok != "" {
- return tok, nil
- }
- generator, err := token.NewGenerator(false, false)
- if err != nil {
- return "", err
- }
- sess, err := session.NewSessionWithOptions(session.Options{
- SharedConfigState: session.SharedConfigEnable,
- Config: aws.Config{
- Credentials: credentials.NewStaticCredentials(
- sa.AWSAccessKeyID,
- sa.AWSSecretAccessKey,
- "",
- ),
- },
- })
- if err != nil {
- return "", err
- }
- tok, err := generator.GetWithOptions(&token.GetTokenOptions{
- Session: sess,
- ClusterID: sa.AWSClusterID,
- })
- if err != nil {
- return "", err
- }
- updateTokenCache(tok.Token, tok.Expiration)
- return tok.Token, nil
- }
- // GetRestrictedClientConfigFromBytes returns a clientcmd.ClientConfig from a raw kubeconfig,
- // a context name, and the set of allowed contexts.
- func GetRestrictedClientConfigFromBytes(
- bytes []byte,
- contextName string,
- allowedContexts []string,
- ) (clientcmd.ClientConfig, error) {
- config, err := clientcmd.NewClientConfigFromBytes(bytes)
- if err != nil {
- return nil, err
- }
- rawConf, err := config.RawConfig()
- if err != nil {
- return nil, err
- }
- // grab a copy to get the pointer and set clusters, authinfos, and contexts to empty
- copyConf := rawConf.DeepCopy()
- copyConf.Clusters = make(map[string]*api.Cluster)
- copyConf.AuthInfos = make(map[string]*api.AuthInfo)
- copyConf.Contexts = make(map[string]*api.Context)
- copyConf.CurrentContext = contextName
- // put allowed clusters in a map
- aContextMap := CreateAllowedContextMap(allowedContexts)
- context, ok := rawConf.Contexts[contextName]
- if ok {
- userName := context.AuthInfo
- clusterName := context.Cluster
- authInfo, userFound := rawConf.AuthInfos[userName]
- cluster, clusterFound := rawConf.Clusters[clusterName]
- // make sure the cluster is "allowed"
- _, isAllowed := aContextMap[contextName]
- if userFound && clusterFound && isAllowed {
- copyConf.Clusters[clusterName] = cluster
- copyConf.AuthInfos[userName] = authInfo
- copyConf.Contexts[contextName] = context
- }
- }
- // validate the copyConf and create a ClientConfig
- err = clientcmd.Validate(*copyConf)
- if err != nil {
- return nil, err
- }
- clientConf := clientcmd.NewDefaultClientConfig(*copyConf, &clientcmd.ConfigOverrides{})
- return clientConf, nil
- }
- // GetContextsFromBytes converts a raw string to a set of Contexts
- // by unmarshaling and calling toContexts
- func GetContextsFromBytes(bytes []byte, allowedContexts []string) ([]models.Context, error) {
- config, err := clientcmd.NewClientConfigFromBytes(bytes)
- if err != nil {
- return nil, err
- }
- rawConf, err := config.RawConfig()
- if err != nil {
- return nil, err
- }
- err = clientcmd.Validate(rawConf)
- if err != nil {
- return nil, err
- }
- contexts := toContexts(&rawConf, allowedContexts)
- return contexts, nil
- }
- func toContexts(rawConf *api.Config, allowedContexts []string) []models.Context {
- contexts := make([]models.Context, 0)
- // put allowed clusters in map
- aContextMap := CreateAllowedContextMap(allowedContexts)
- // iterate through contexts and switch on selected
- for name, context := range rawConf.Contexts {
- _, isAllowed := aContextMap[name]
- _, userFound := rawConf.AuthInfos[context.AuthInfo]
- cluster, clusterFound := rawConf.Clusters[context.Cluster]
- if userFound && clusterFound && isAllowed {
- contexts = append(contexts, models.Context{
- Name: name,
- Server: cluster.Server,
- Cluster: context.Cluster,
- User: context.AuthInfo,
- Selected: true,
- })
- } else if userFound && clusterFound {
- contexts = append(contexts, models.Context{
- Name: name,
- Server: cluster.Server,
- Cluster: context.Cluster,
- User: context.AuthInfo,
- Selected: false,
- })
- }
- }
- return contexts
- }
- // CreateAllowedContextMap creates a dummy map from context name to context name
- func CreateAllowedContextMap(contexts []string) map[string]string {
- aContextMap := make(map[string]string)
- for _, context := range contexts {
- aContextMap[context] = context
- }
- return aContextMap
- }
|