kubeconfig.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. package kubernetes
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net/url"
  6. "github.com/porter-dev/porter/api/types"
  7. "github.com/porter-dev/porter/internal/models"
  8. "k8s.io/client-go/tools/clientcmd"
  9. "k8s.io/client-go/tools/clientcmd/api"
  10. )
  11. // GetClusterCandidatesFromKubeconfig parses a kubeconfig for a list of cluster
  12. // candidates.
  13. //
  14. // The local boolean represents whether the auth mechanism should be designated as
  15. // "local": if so, the auth mechanism uses local plugins/mechanisms purely from the
  16. // kubeconfig.
  17. func GetClusterCandidatesFromKubeconfig(
  18. kubeconfig []byte,
  19. projectID uint,
  20. local bool,
  21. ) ([]*models.ClusterCandidate, error) {
  22. config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
  23. if err != nil {
  24. return nil, err
  25. }
  26. rawConf, err := config.RawConfig()
  27. if err != nil {
  28. return nil, err
  29. }
  30. res := make([]*models.ClusterCandidate, 0)
  31. for contextName, context := range rawConf.Contexts {
  32. clusterName := context.Cluster
  33. awsClusterID := ""
  34. authInfoName := context.AuthInfo
  35. resolvers := make([]models.ClusterResolver, 0)
  36. var authMechanism models.ClusterAuth
  37. if local {
  38. authMechanism = models.Local
  39. } else {
  40. // get the resolvers, if needed
  41. authMechanism, resolvers = parseAuthInfoForResolvers(rawConf.AuthInfos[authInfoName])
  42. clusterResolvers := parseClusterForResolvers(rawConf.Clusters[clusterName])
  43. resolvers = append(resolvers, clusterResolvers...)
  44. if authMechanism == models.AWS {
  45. // if the auth mechanism is AWS, we need to parse more explicitly
  46. // for the cluster id
  47. awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
  48. }
  49. }
  50. // construct the raw kubeconfig that's relevant for that context
  51. contextConf, err := getConfigForContext(&rawConf, contextName)
  52. if err != nil {
  53. continue
  54. }
  55. rawBytes, err := clientcmd.Write(*contextConf)
  56. if err == nil {
  57. // create the candidate cluster
  58. res = append(res, &models.ClusterCandidate{
  59. AuthMechanism: authMechanism,
  60. ProjectID: projectID,
  61. Resolvers: resolvers,
  62. ContextName: contextName,
  63. Name: clusterName,
  64. Server: rawConf.Clusters[clusterName].Server,
  65. AWSClusterIDGuess: []byte(awsClusterID),
  66. Kubeconfig: rawBytes,
  67. })
  68. }
  69. }
  70. return res, nil
  71. }
  72. // GetRawConfigFromBytes returns the clientcmdapi.Config from kubeconfig
  73. // bytes
  74. func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
  75. config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
  76. if err != nil {
  77. return nil, err
  78. }
  79. rawConf, err := config.RawConfig()
  80. if err != nil {
  81. return nil, err
  82. }
  83. return &rawConf, nil
  84. }
  85. // Parsing rules are:
  86. //
  87. // (1) If a client certificate + client key exist, uses x509 auth mechanism
  88. // (2) If an oidc/gcp/aws plugin exists, uses that auth mechanism
  89. // (3) If a bearer token exists, uses bearer token auth mechanism
  90. // (4) If a username/password exist, uses basic auth mechanism
  91. // (5) Otherwise, the config gets skipped
  92. //
  93. func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.ClusterAuth, resolvers []models.ClusterResolver) {
  94. resolvers = make([]models.ClusterResolver, 0)
  95. if (authInfo.ClientCertificate != "" || len(authInfo.ClientCertificateData) != 0) &&
  96. (authInfo.ClientKey != "" || len(authInfo.ClientKeyData) != 0) {
  97. if len(authInfo.ClientCertificateData) == 0 {
  98. fn := map[string]string{
  99. "filename": authInfo.ClientCertificate,
  100. }
  101. fnBytes, _ := json.Marshal(&fn)
  102. resolvers = append(resolvers, models.ClusterResolver{
  103. Name: types.ClientCertData,
  104. Resolved: false,
  105. Data: fnBytes,
  106. })
  107. }
  108. if len(authInfo.ClientKeyData) == 0 {
  109. fn := map[string]string{
  110. "filename": authInfo.ClientKey,
  111. }
  112. fnBytes, _ := json.Marshal(&fn)
  113. resolvers = append(resolvers, models.ClusterResolver{
  114. Name: types.ClientKeyData,
  115. Resolved: false,
  116. Data: fnBytes,
  117. })
  118. }
  119. return models.X509, resolvers
  120. }
  121. if authInfo.AuthProvider != nil {
  122. switch authInfo.AuthProvider.Name {
  123. case "oidc":
  124. filename, isFile := authInfo.AuthProvider.Config["idp-certificate-authority"]
  125. data, isData := authInfo.AuthProvider.Config["idp-certificate-authority-data"]
  126. if isFile && (!isData || data == "") {
  127. fn := map[string]string{
  128. "filename": filename,
  129. }
  130. fnBytes, _ := json.Marshal(&fn)
  131. return models.OIDC, []models.ClusterResolver{
  132. {
  133. Name: types.OIDCIssuerData,
  134. Resolved: false,
  135. Data: fnBytes,
  136. },
  137. }
  138. }
  139. return models.OIDC, resolvers
  140. case "gcp":
  141. return models.GCP, []models.ClusterResolver{
  142. {
  143. Name: types.GCPKeyData,
  144. Resolved: false,
  145. },
  146. }
  147. }
  148. }
  149. if authInfo.Exec != nil {
  150. if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
  151. return models.AWS, []models.ClusterResolver{
  152. {
  153. Name: types.AWSData,
  154. Resolved: false,
  155. },
  156. }
  157. }
  158. }
  159. if authInfo.Token != "" || authInfo.TokenFile != "" {
  160. if authInfo.Token == "" {
  161. fn := map[string]string{
  162. "filename": authInfo.TokenFile,
  163. }
  164. fnBytes, _ := json.Marshal(&fn)
  165. return models.Bearer, []models.ClusterResolver{
  166. {
  167. Name: types.TokenData,
  168. Resolved: false,
  169. Data: fnBytes,
  170. },
  171. }
  172. }
  173. return models.Bearer, resolvers
  174. }
  175. if authInfo.Username != "" && authInfo.Password != "" {
  176. return models.Basic, resolvers
  177. }
  178. return models.X509, resolvers
  179. }
  180. // Parses the cluster object to determine resolvers -- only currently supported resolver is
  181. // population of the cluster certificate authority data
  182. func parseClusterForResolvers(cluster *api.Cluster) (resolvers []models.ClusterResolver) {
  183. resolvers = make([]models.ClusterResolver, 0)
  184. if cluster.CertificateAuthority != "" && len(cluster.CertificateAuthorityData) == 0 {
  185. fn := map[string]string{
  186. "filename": cluster.CertificateAuthority,
  187. }
  188. fnBytes, _ := json.Marshal(&fn)
  189. resolvers = append(resolvers, models.ClusterResolver{
  190. Name: types.ClusterCAData,
  191. Resolved: false,
  192. Data: fnBytes,
  193. })
  194. }
  195. serverURL, err := url.Parse(cluster.Server)
  196. if err == nil {
  197. if hostname := serverURL.Hostname(); hostname == "127.0.0.1" || hostname == "localhost" {
  198. resolvers = append(resolvers, models.ClusterResolver{
  199. Name: types.ClusterLocalhost,
  200. Resolved: false,
  201. })
  202. }
  203. }
  204. return resolvers
  205. }
  206. func parseAuthInfoForAWSClusterID(authInfo *api.AuthInfo, fallback string) string {
  207. if authInfo.Exec != nil {
  208. if authInfo.Exec.Command == "aws" {
  209. // look for --cluster-name flag
  210. for i, arg := range authInfo.Exec.Args {
  211. if arg == "--cluster-name" && len(authInfo.Exec.Args) > i+1 {
  212. return authInfo.Exec.Args[i+1]
  213. }
  214. }
  215. } else if authInfo.Exec.Command == "aws-iam-authenticator" {
  216. // look for -i or --cluster-id flag
  217. for i, arg := range authInfo.Exec.Args {
  218. if (arg == "-i" || arg == "--cluster-id") && len(authInfo.Exec.Args) > i+1 {
  219. return authInfo.Exec.Args[i+1]
  220. }
  221. }
  222. }
  223. }
  224. return fallback
  225. }
  226. // getConfigForContext returns the raw kubeconfig associated with only a
  227. // single context of the raw config
  228. func getConfigForContext(
  229. rawConf *api.Config,
  230. contextName string,
  231. ) (*api.Config, error) {
  232. copyConf := rawConf.DeepCopy()
  233. copyConf.Clusters = make(map[string]*api.Cluster)
  234. copyConf.AuthInfos = make(map[string]*api.AuthInfo)
  235. copyConf.Contexts = make(map[string]*api.Context)
  236. copyConf.CurrentContext = contextName
  237. context, ok := rawConf.Contexts[contextName]
  238. if ok {
  239. userName := context.AuthInfo
  240. clusterName := context.Cluster
  241. authInfo, userFound := rawConf.AuthInfos[userName]
  242. cluster, clusterFound := rawConf.Clusters[clusterName]
  243. if userFound && clusterFound {
  244. copyConf.Clusters[clusterName] = cluster
  245. copyConf.AuthInfos[userName] = authInfo
  246. copyConf.Contexts[contextName] = context
  247. } else {
  248. return nil, errors.New("linked user and cluster not found")
  249. }
  250. } else {
  251. return nil, errors.New("context not found")
  252. }
  253. return copyConf, nil
  254. }
  255. // CreateAllowedContextMap creates a dummy map from context name to context name
  256. func CreateAllowedContextMap(contexts []string) map[string]string {
  257. aContextMap := make(map[string]string)
  258. for _, context := range contexts {
  259. aContextMap[context] = context
  260. }
  261. return aContextMap
  262. }