kubeconfig.go 10 KB

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