kubeconfig.go 13 KB

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