kubeconfig.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. package connect
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "io/ioutil"
  8. "os"
  9. "strings"
  10. "github.com/fatih/color"
  11. "github.com/porter-dev/porter/cli/cmd/utils"
  12. "github.com/porter-dev/porter/internal/kubernetes/local"
  13. awsLocal "github.com/porter-dev/porter/internal/providers/aws/local"
  14. gcpLocal "github.com/porter-dev/porter/internal/providers/gcp/local"
  15. "github.com/porter-dev/porter/cli/cmd/api"
  16. "github.com/porter-dev/porter/internal/models"
  17. )
  18. // Kubeconfig creates a service account for a project by parsing the local
  19. // kubeconfig and resolving actions that must be performed.
  20. func Kubeconfig(
  21. client *api.Client,
  22. kubeconfigPath string,
  23. contexts []string,
  24. projectID uint,
  25. ) error {
  26. // if project ID is 0, ask the user to set the project ID or create a project
  27. if projectID == 0 {
  28. return fmt.Errorf("no project set, please run porter project set [id]")
  29. }
  30. // get the kubeconfig
  31. rawBytes, err := local.GetKubeconfigFromHost(kubeconfigPath, contexts)
  32. if err != nil {
  33. return err
  34. }
  35. // send kubeconfig to client
  36. saCandidates, err := client.CreateProjectCandidates(
  37. context.Background(),
  38. projectID,
  39. &api.CreateProjectCandidatesRequest{
  40. Kubeconfig: string(rawBytes),
  41. },
  42. )
  43. if err != nil {
  44. return err
  45. }
  46. for _, saCandidate := range saCandidates {
  47. var clusters []models.ClusterExternal
  48. var saID uint
  49. if len(saCandidate.Actions) > 0 {
  50. resolvers := make(api.CreateProjectServiceAccountRequest, 0)
  51. for _, action := range saCandidate.Actions {
  52. switch action.Name {
  53. case models.ClusterCADataAction:
  54. absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
  55. if err != nil {
  56. return err
  57. }
  58. filename, err := utils.GetFileReferenceFromKubeconfig(
  59. action.Filename,
  60. absKubeconfigPath,
  61. )
  62. if err != nil {
  63. return err
  64. }
  65. resolveAction, err := resolveClusterCAAction(filename)
  66. if err != nil {
  67. return err
  68. }
  69. resolvers = append(resolvers, resolveAction)
  70. case models.ClientCertDataAction:
  71. absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
  72. if err != nil {
  73. return err
  74. }
  75. filename, err := utils.GetFileReferenceFromKubeconfig(
  76. action.Filename,
  77. absKubeconfigPath,
  78. )
  79. if err != nil {
  80. return err
  81. }
  82. resolveAction, err := resolveClientCertAction(filename)
  83. if err != nil {
  84. return err
  85. }
  86. resolvers = append(resolvers, resolveAction)
  87. case models.ClientKeyDataAction:
  88. absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
  89. if err != nil {
  90. return err
  91. }
  92. filename, err := utils.GetFileReferenceFromKubeconfig(
  93. action.Filename,
  94. absKubeconfigPath,
  95. )
  96. if err != nil {
  97. return err
  98. }
  99. resolveAction, err := resolveClientKeyAction(filename)
  100. if err != nil {
  101. return err
  102. }
  103. resolvers = append(resolvers, resolveAction)
  104. case models.OIDCIssuerDataAction:
  105. absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
  106. if err != nil {
  107. return err
  108. }
  109. filename, err := utils.GetFileReferenceFromKubeconfig(
  110. action.Filename,
  111. absKubeconfigPath,
  112. )
  113. if err != nil {
  114. return err
  115. }
  116. resolveAction, err := resolveOIDCIssuerAction(filename)
  117. if err != nil {
  118. return err
  119. }
  120. resolvers = append(resolvers, resolveAction)
  121. case models.TokenDataAction:
  122. absKubeconfigPath, err := local.ResolveKubeconfigPath(kubeconfigPath)
  123. if err != nil {
  124. return err
  125. }
  126. filename, err := utils.GetFileReferenceFromKubeconfig(
  127. action.Filename,
  128. absKubeconfigPath,
  129. )
  130. if err != nil {
  131. return err
  132. }
  133. resolveAction, err := resolveTokenDataAction(filename)
  134. if err != nil {
  135. return err
  136. }
  137. resolvers = append(resolvers, resolveAction)
  138. case models.GCPKeyDataAction:
  139. resolveAction, err := resolveGCPKeyAction(
  140. saCandidate.ClusterEndpoint,
  141. saCandidate.ClusterName,
  142. )
  143. if err != nil {
  144. return err
  145. }
  146. resolvers = append(resolvers, resolveAction)
  147. case models.AWSDataAction:
  148. resolveAction, err := resolveAWSAction(
  149. saCandidate.ClusterEndpoint,
  150. saCandidate.ClusterName,
  151. saCandidate.AWSClusterIDGuess,
  152. kubeconfigPath,
  153. saCandidate.ContextName,
  154. )
  155. if err != nil {
  156. return err
  157. }
  158. resolvers = append(resolvers, resolveAction)
  159. }
  160. }
  161. sa, err := client.CreateProjectServiceAccount(
  162. context.Background(),
  163. projectID,
  164. saCandidate.ID,
  165. resolvers,
  166. )
  167. if err != nil {
  168. return err
  169. }
  170. clusters = sa.Clusters
  171. saID = sa.ID
  172. } else {
  173. sa, err := client.GetProjectServiceAccount(
  174. context.Background(),
  175. projectID,
  176. saCandidate.CreatedServiceAccountID,
  177. )
  178. if err != nil {
  179. return err
  180. }
  181. clusters = sa.Clusters
  182. saID = sa.ID
  183. }
  184. for _, cluster := range clusters {
  185. color.New(color.FgGreen).Printf("created service account for cluster %s with id %d\n", cluster.Name, saID)
  186. }
  187. }
  188. return nil
  189. }
  190. // resolves a cluster ca data action
  191. func resolveClusterCAAction(
  192. filename string,
  193. ) (*models.ServiceAccountAllActions, error) {
  194. fileBytes, err := ioutil.ReadFile(filename)
  195. if err != nil {
  196. return nil, err
  197. }
  198. return &models.ServiceAccountAllActions{
  199. Name: models.ClusterCADataAction,
  200. ClusterCAData: base64.StdEncoding.EncodeToString(fileBytes),
  201. }, nil
  202. }
  203. // resolves a client cert data action
  204. func resolveClientCertAction(
  205. filename string,
  206. ) (*models.ServiceAccountAllActions, error) {
  207. fileBytes, err := ioutil.ReadFile(filename)
  208. if err != nil {
  209. return nil, err
  210. }
  211. return &models.ServiceAccountAllActions{
  212. Name: models.ClientCertDataAction,
  213. ClientCertData: base64.StdEncoding.EncodeToString(fileBytes),
  214. }, nil
  215. }
  216. // resolves a client key data action
  217. func resolveClientKeyAction(
  218. filename string,
  219. ) (*models.ServiceAccountAllActions, error) {
  220. fileBytes, err := ioutil.ReadFile(filename)
  221. if err != nil {
  222. return nil, err
  223. }
  224. return &models.ServiceAccountAllActions{
  225. Name: models.ClientKeyDataAction,
  226. ClientKeyData: base64.StdEncoding.EncodeToString(fileBytes),
  227. }, nil
  228. }
  229. // resolves an oidc issuer data action
  230. func resolveOIDCIssuerAction(
  231. filename string,
  232. ) (*models.ServiceAccountAllActions, error) {
  233. fileBytes, err := ioutil.ReadFile(filename)
  234. if err != nil {
  235. return nil, err
  236. }
  237. return &models.ServiceAccountAllActions{
  238. Name: models.OIDCIssuerDataAction,
  239. OIDCIssuerCAData: base64.StdEncoding.EncodeToString(fileBytes),
  240. }, nil
  241. }
  242. // resolves a token data action
  243. func resolveTokenDataAction(
  244. filename string,
  245. ) (*models.ServiceAccountAllActions, error) {
  246. fileBytes, err := ioutil.ReadFile(filename)
  247. if err != nil {
  248. return nil, err
  249. }
  250. return &models.ServiceAccountAllActions{
  251. Name: models.TokenDataAction,
  252. TokenData: string(fileBytes),
  253. }, nil
  254. }
  255. // resolves a gcp key data action
  256. func resolveGCPKeyAction(endpoint string, clusterName string) (*models.ServiceAccountAllActions, error) {
  257. userResp, err := utils.PromptPlaintext(
  258. fmt.Sprintf(
  259. `Detected GKE cluster in kubeconfig for the endpoint %s (%s).
  260. Porter can set up a service account in your GCP project to connect to this cluster automatically.
  261. Would you like to proceed? %s `,
  262. color.New(color.FgCyan).Sprintf("%s", endpoint),
  263. clusterName,
  264. color.New(color.FgCyan).Sprintf("[y/n]"),
  265. ),
  266. )
  267. if err != nil {
  268. return nil, err
  269. }
  270. if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
  271. agent, err := gcpLocal.NewDefaultAgent()
  272. if err != nil {
  273. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  274. return resolveGCPKeyActionManual(endpoint, clusterName)
  275. }
  276. projID, err := agent.GetProjectIDForGKECluster(endpoint)
  277. if err != nil {
  278. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  279. return resolveGCPKeyActionManual(endpoint, clusterName)
  280. }
  281. agent.ProjectID = projID
  282. name := "porter-dashboard-" + utils.StringWithCharset(6, "abcdefghijklmnopqrstuvwxyz1234567890")
  283. // create the service account and give it the correct iam permissions
  284. resp, err := agent.CreateServiceAccount(name)
  285. if err != nil {
  286. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  287. return resolveGCPKeyActionManual(endpoint, clusterName)
  288. }
  289. err = agent.SetServiceAccountIAMPolicy(resp)
  290. if err != nil {
  291. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  292. return resolveGCPKeyActionManual(endpoint, clusterName)
  293. }
  294. // get the service account key data to send to the server
  295. bytes, err := agent.CreateServiceAccountKey(resp)
  296. if err != nil {
  297. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  298. return resolveGCPKeyActionManual(endpoint, clusterName)
  299. }
  300. return &models.ServiceAccountAllActions{
  301. Name: models.GCPKeyDataAction,
  302. GCPKeyData: string(bytes),
  303. }, nil
  304. }
  305. return resolveGCPKeyActionManual(endpoint, clusterName)
  306. }
  307. func resolveGCPKeyActionManual(endpoint string, clusterName string) (*models.ServiceAccountAllActions, error) {
  308. keyFileLocation, err := utils.PromptPlaintext(fmt.Sprintf(`Please provide the full path to a service account key file.
  309. Key file location: `))
  310. if err != nil {
  311. return nil, err
  312. }
  313. // attempt to read the key file location
  314. if info, err := os.Stat(keyFileLocation); !os.IsNotExist(err) && !info.IsDir() {
  315. // read the file
  316. bytes, err := ioutil.ReadFile(keyFileLocation)
  317. if err != nil {
  318. return nil, err
  319. }
  320. return &models.ServiceAccountAllActions{
  321. Name: models.GCPKeyDataAction,
  322. GCPKeyData: string(bytes),
  323. }, nil
  324. }
  325. return nil, errors.New("Key file not found")
  326. }
  327. // resolves an aws key data action
  328. func resolveAWSAction(
  329. endpoint string,
  330. clusterName string,
  331. awsClusterIDGuess string,
  332. kubeconfigPath string,
  333. contextName string,
  334. ) (*models.ServiceAccountAllActions, error) {
  335. userResp, err := utils.PromptPlaintext(
  336. fmt.Sprintf(
  337. `Detected AWS cluster in kubeconfig for the endpoint %s (%s).
  338. Porter can set up an IAM user in your AWS account to connect to this cluster automatically.
  339. Would you like to proceed? %s `,
  340. color.New(color.FgCyan).Sprintf("%s", endpoint),
  341. clusterName,
  342. color.New(color.FgCyan).Sprintf("[y/n]"),
  343. ),
  344. )
  345. if err != nil {
  346. return nil, err
  347. }
  348. if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
  349. agent, err := awsLocal.NewDefaultAgent(kubeconfigPath, contextName)
  350. if err != nil {
  351. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  352. return resolveAWSActionManual(endpoint, clusterName, awsClusterIDGuess)
  353. }
  354. creds, err := agent.CreateIAMKubernetesMapping(awsClusterIDGuess)
  355. if err != nil {
  356. color.New(color.FgRed).Printf("Automatic creation failed, manual input required. Error was: %v\n", err)
  357. return resolveAWSActionManual(endpoint, clusterName, awsClusterIDGuess)
  358. }
  359. return &models.ServiceAccountAllActions{
  360. Name: models.AWSDataAction,
  361. AWSAccessKeyID: creds.AWSAccessKeyID,
  362. AWSSecretAccessKey: creds.AWSSecretAccessKey,
  363. AWSClusterID: creds.AWSClusterID,
  364. }, nil
  365. }
  366. // fallback to manual
  367. return resolveAWSActionManual(endpoint, clusterName, awsClusterIDGuess)
  368. }
  369. func resolveAWSActionManual(
  370. endpoint string,
  371. clusterName string,
  372. awsClusterIDGuess string,
  373. ) (*models.ServiceAccountAllActions, error) {
  374. // query to see if the AWS cluster ID guess is correct
  375. var clusterID string
  376. userResp, err := utils.PromptPlaintext(
  377. fmt.Sprintf(
  378. `Detected AWS cluster ID as %s. Is this correct? %s `,
  379. color.New(color.FgCyan).Sprintf(awsClusterIDGuess),
  380. color.New(color.FgCyan).Sprintf("[y/n]"),
  381. ),
  382. )
  383. if err != nil {
  384. return nil, err
  385. }
  386. if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
  387. clusterID = awsClusterIDGuess
  388. } else {
  389. clusterID, err = utils.PromptPlaintext(fmt.Sprintf(`Cluster ID: `))
  390. if err != nil {
  391. return nil, err
  392. }
  393. }
  394. // query for the access key id
  395. accessKeyID, err := utils.PromptPlaintext(fmt.Sprintf(`AWS Access Key ID: `))
  396. if err != nil {
  397. return nil, err
  398. }
  399. // query for the secret access key
  400. secretKey, err := utils.PromptPlaintext(fmt.Sprintf(`AWS Secret Access Key: `))
  401. if err != nil {
  402. return nil, err
  403. }
  404. return &models.ServiceAccountAllActions{
  405. Name: models.AWSDataAction,
  406. AWSAccessKeyID: accessKeyID,
  407. AWSSecretAccessKey: secretKey,
  408. AWSClusterID: clusterID,
  409. }, nil
  410. }