create_resource.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. package state
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "regexp"
  10. "strings"
  11. "github.com/aws/aws-sdk-go/service/ecr"
  12. "github.com/porter-dev/porter/api/server/shared"
  13. "github.com/porter-dev/porter/api/server/shared/apierrors"
  14. "github.com/porter-dev/porter/api/types"
  15. "github.com/porter-dev/porter/internal/analytics"
  16. "github.com/porter-dev/porter/internal/features"
  17. "github.com/porter-dev/porter/internal/kubernetes"
  18. "github.com/porter-dev/porter/internal/kubernetes/envgroup"
  19. "github.com/porter-dev/porter/internal/models"
  20. "github.com/porter-dev/porter/internal/telemetry"
  21. "github.com/porter-dev/porter/provisioner/integrations/redis_stream"
  22. "github.com/porter-dev/porter/provisioner/server/config"
  23. ptypes "github.com/porter-dev/porter/provisioner/types"
  24. "gorm.io/gorm"
  25. )
  26. type CreateResourceHandler struct {
  27. Config *config.Config
  28. decoderValidator shared.RequestDecoderValidator
  29. }
  30. func NewCreateResourceHandler(
  31. config *config.Config,
  32. ) *CreateResourceHandler {
  33. return &CreateResourceHandler{
  34. Config: config,
  35. decoderValidator: shared.NewDefaultRequestDecoderValidator(config.Logger, config.Alerter),
  36. }
  37. }
  38. func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  39. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-provisioner-resource")
  40. defer span.End()
  41. // read the infra from the attached scope
  42. infra, _ := ctx.Value(types.InfraScope).(*models.Infra)
  43. operation, _ := ctx.Value(types.OperationScope).(*models.Operation)
  44. req := &ptypes.CreateResourceRequest{}
  45. if ok := c.decoderValidator.DecodeAndValidate(w, r, req); !ok {
  46. return
  47. }
  48. // update the operation to indicate completion
  49. operation.Status = "completed"
  50. operation, err := c.Config.Repo.Infra().UpdateOperation(operation)
  51. if err != nil {
  52. apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
  53. return
  54. }
  55. // push to the operation stream
  56. err = redis_stream.SendOperationCompleted(c.Config.RedisClient, infra, operation)
  57. if err != nil {
  58. apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
  59. return
  60. }
  61. // push to the global stream
  62. err = redis_stream.PushToGlobalStream(c.Config.RedisClient, infra, operation, "created")
  63. if err != nil {
  64. apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
  65. return
  66. }
  67. // update the infra to indicate completion
  68. infra.Status = "created"
  69. infra, err = c.Config.Repo.Infra().UpdateInfra(infra)
  70. if err != nil {
  71. apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
  72. return
  73. }
  74. // switch on the kind of resource and write the corresponding objects to the database
  75. switch req.Kind {
  76. case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS):
  77. var cluster *models.Cluster
  78. cluster, err = createCluster(c.Config, infra, c.Config.LaunchDarklyClient, req.Output)
  79. if cluster != nil {
  80. c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningSuccessTrack(
  81. &analytics.ClusterProvisioningSuccessTrackOpts{
  82. ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(0, infra.ProjectID, cluster.ID),
  83. ClusterType: infra.Kind,
  84. InfraID: infra.ID,
  85. },
  86. ))
  87. }
  88. case string(types.InfraECR):
  89. _, err = createECRRegistry(c.Config, infra, operation, req.Output)
  90. case string(types.InfraRDS):
  91. _, err = createRDSDatabase(ctx, c.Config, infra, operation, req.Output)
  92. case string(types.InfraS3):
  93. err = createS3Bucket(ctx, c.Config, infra, operation, req.Output)
  94. case string(types.InfraDOCR):
  95. _, err = createDOCRRegistry(c.Config, infra, operation, req.Output)
  96. case string(types.InfraGCR):
  97. _, err = createGCRRegistry(c.Config, infra, operation, req.Output)
  98. case string(types.InfraGAR):
  99. _, err = createGARRegistry(c.Config, infra, operation, req.Output)
  100. case string(types.InfraACR):
  101. _, err = createACRRegistry(c.Config, infra, operation, req.Output)
  102. }
  103. if err != nil {
  104. apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
  105. return
  106. }
  107. }
  108. func createECRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
  109. reg := &models.Registry{
  110. ProjectID: infra.ProjectID,
  111. AWSIntegrationID: infra.AWSIntegrationID,
  112. InfraID: infra.ID,
  113. Name: output["name"].(string),
  114. }
  115. // parse raw data into ECR type
  116. awsInt, err := config.Repo.AWSIntegration().ReadAWSIntegration(reg.ProjectID, reg.AWSIntegrationID)
  117. if err != nil {
  118. return nil, err
  119. }
  120. sess, err := awsInt.GetSession()
  121. if err != nil {
  122. return nil, err
  123. }
  124. ecrSvc := ecr.New(sess)
  125. authOutput, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
  126. if err != nil {
  127. return nil, err
  128. }
  129. reg.URL = *authOutput.AuthorizationData[0].ProxyEndpoint
  130. reg, err = config.Repo.Registry().CreateRegistry(reg)
  131. if err != nil {
  132. return nil, err
  133. }
  134. return reg, nil
  135. }
  136. func createRDSDatabase(ctx context.Context, config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Database, error) {
  137. // check for infra id being 0 as a safeguard so that all non-provisioned
  138. // clusters are not matched by read
  139. if infra.ID == 0 {
  140. return nil, fmt.Errorf("infra id cannot be 0")
  141. }
  142. var database *models.Database
  143. var err error
  144. var isNotFound bool
  145. database, err = config.Repo.Database().ReadDatabaseByInfraID(infra.ProjectID, infra.ID)
  146. isNotFound = err != nil && errors.Is(err, gorm.ErrRecordNotFound)
  147. if isNotFound {
  148. database = &models.Database{
  149. ProjectID: infra.ProjectID,
  150. ClusterID: infra.ParentClusterID,
  151. InfraID: infra.ID,
  152. Status: "Running",
  153. }
  154. } else if err != nil {
  155. return nil, err
  156. }
  157. database.InstanceID = output["rds_instance_id"].(string)
  158. database.InstanceEndpoint = output["rds_connection_endpoint"].(string)
  159. database.InstanceName = output["rds_instance_name"].(string)
  160. if isNotFound {
  161. database, err = config.Repo.Database().CreateDatabase(database)
  162. } else {
  163. database, err = config.Repo.Database().UpdateDatabase(database)
  164. }
  165. infra.DatabaseID = database.ID
  166. infra, err = config.Repo.Infra().UpdateInfra(infra)
  167. if err != nil {
  168. return nil, err
  169. }
  170. lastApplied := make(map[string]interface{})
  171. err = json.Unmarshal(operation.LastApplied, &lastApplied)
  172. if err != nil {
  173. return nil, err
  174. }
  175. err = createRDSEnvGroup(ctx, config, infra, database, lastApplied)
  176. if err != nil {
  177. return nil, err
  178. }
  179. return database, nil
  180. }
  181. func createS3Bucket(ctx context.Context, config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) error {
  182. lastApplied := make(map[string]interface{})
  183. err := json.Unmarshal(operation.LastApplied, &lastApplied)
  184. if err != nil {
  185. return err
  186. }
  187. return createS3EnvGroup(ctx, config, infra, lastApplied, output)
  188. }
  189. func createCluster(config *config.Config, infra *models.Infra, launchDarklyClient *features.Client, output map[string]interface{}) (*models.Cluster, error) {
  190. // check for infra id being 0 as a safeguard so that all non-provisioned
  191. // clusters are not matched by read
  192. if infra.ID == 0 {
  193. return nil, fmt.Errorf("infra id cannot be 0")
  194. }
  195. var cluster *models.Cluster
  196. var err error
  197. var isNotFound bool
  198. // look for cluster matching infra in database; if the cluster already exists, update the cluster but
  199. // don't add it again
  200. cluster, err = config.Repo.Cluster().ReadClusterByInfraID(infra.ProjectID, infra.ID)
  201. isNotFound = err != nil && errors.Is(err, gorm.ErrRecordNotFound)
  202. if isNotFound {
  203. cluster = getNewCluster(infra)
  204. } else if err != nil {
  205. return nil, err
  206. }
  207. caData, err := transformClusterCAData([]byte(output["cluster_ca_data"].(string)))
  208. if err != nil {
  209. return nil, err
  210. }
  211. // if cluster_token is output and infra is azure, update the azure integration
  212. if _, exists := output["cluster_token"]; exists && infra.AzureIntegrationID != 0 {
  213. azInt, err := config.Repo.AzureIntegration().ReadAzureIntegration(infra.ProjectID, infra.AzureIntegrationID)
  214. if err != nil {
  215. return nil, err
  216. }
  217. azInt.AKSPassword = []byte(output["cluster_token"].(string))
  218. azInt, err = config.Repo.AzureIntegration().OverwriteAzureIntegration(azInt)
  219. if err != nil {
  220. return nil, err
  221. }
  222. }
  223. // only update the cluster name if this is during creation - we don't want to overwrite the cluster name
  224. // which may have been manually set
  225. if isNotFound {
  226. cluster.Name = output["cluster_name"].(string)
  227. }
  228. cluster.Server = output["cluster_endpoint"].(string)
  229. cluster.CertificateAuthorityData = caData
  230. if isNotFound {
  231. cluster, err = config.Repo.Cluster().CreateCluster(cluster, launchDarklyClient)
  232. } else {
  233. cluster, err = config.Repo.Cluster().UpdateCluster(cluster, launchDarklyClient)
  234. }
  235. if err != nil {
  236. return nil, err
  237. }
  238. return cluster, nil
  239. }
  240. func getNewCluster(infra *models.Infra) *models.Cluster {
  241. res := &models.Cluster{
  242. ProjectID: infra.ProjectID,
  243. InfraID: infra.ID,
  244. MonitorHelmReleases: true,
  245. }
  246. switch infra.Kind {
  247. case types.InfraEKS:
  248. res.AuthMechanism = models.AWS
  249. res.AWSIntegrationID = infra.AWSIntegrationID
  250. case types.InfraGKE:
  251. res.AuthMechanism = models.GCP
  252. res.GCPIntegrationID = infra.GCPIntegrationID
  253. case types.InfraDOKS:
  254. res.AuthMechanism = models.DO
  255. res.DOIntegrationID = infra.DOIntegrationID
  256. case types.InfraAKS:
  257. res.AuthMechanism = models.Azure
  258. res.AzureIntegrationID = infra.AzureIntegrationID
  259. }
  260. return res
  261. }
  262. func transformClusterCAData(ca []byte) ([]byte, error) {
  263. re := regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
  264. // if it matches the base64 regex, decode it
  265. caData := string(ca)
  266. if re.MatchString(caData) {
  267. decoded, err := base64.StdEncoding.DecodeString(caData)
  268. if err != nil {
  269. return nil, err
  270. }
  271. return []byte(decoded), nil
  272. }
  273. // otherwise just return the CA
  274. return ca, nil
  275. }
  276. func createDOCRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
  277. reg := &models.Registry{
  278. ProjectID: infra.ProjectID,
  279. DOIntegrationID: infra.DOIntegrationID,
  280. InfraID: infra.ID,
  281. URL: output["url"].(string),
  282. Name: output["name"].(string),
  283. }
  284. return config.Repo.Registry().CreateRegistry(reg)
  285. }
  286. func createGCRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
  287. reg := &models.Registry{
  288. ProjectID: infra.ProjectID,
  289. GCPIntegrationID: infra.GCPIntegrationID,
  290. InfraID: infra.ID,
  291. URL: output["url"].(string),
  292. Name: "gcr-registry",
  293. }
  294. return config.Repo.Registry().CreateRegistry(reg)
  295. }
  296. func createGARRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
  297. reg := &models.Registry{
  298. ProjectID: infra.ProjectID,
  299. GCPIntegrationID: infra.GCPIntegrationID,
  300. InfraID: infra.ID,
  301. URL: output["url"].(string),
  302. Name: "gar-registry",
  303. }
  304. return config.Repo.Registry().CreateRegistry(reg)
  305. }
  306. func createACRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
  307. reg := &models.Registry{
  308. ProjectID: infra.ProjectID,
  309. AzureIntegrationID: infra.AzureIntegrationID,
  310. InfraID: infra.ID,
  311. URL: output["url"].(string),
  312. Name: output["name"].(string),
  313. }
  314. return config.Repo.Registry().CreateRegistry(reg)
  315. }
  316. func createRDSEnvGroup(ctx context.Context, config *config.Config, infra *models.Infra, database *models.Database, lastApplied map[string]interface{}) error {
  317. cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
  318. if err != nil {
  319. return err
  320. }
  321. ooc := &kubernetes.OutOfClusterConfig{
  322. Repo: config.Repo,
  323. DigitalOceanOAuth: config.DOConf,
  324. Cluster: cluster,
  325. }
  326. agent, err := kubernetes.GetAgentOutOfClusterConfig(ctx, ooc)
  327. if err != nil {
  328. return fmt.Errorf("failed to get agent: %s", err.Error())
  329. }
  330. // split the instance endpoint on the port
  331. port := "5432"
  332. host := database.InstanceEndpoint
  333. if strArr := strings.Split(database.InstanceEndpoint, ":"); len(strArr) == 2 {
  334. host = strArr[0]
  335. port = strArr[1]
  336. }
  337. _, err = envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
  338. Name: fmt.Sprintf("rds-credentials-%s", lastApplied["db_name"].(string)),
  339. Namespace: "default",
  340. Variables: map[string]string{},
  341. SecretVariables: map[string]string{
  342. "PGPORT": port,
  343. "PGHOST": host,
  344. "PGPASSWORD": lastApplied["db_passwd"].(string),
  345. "PGUSER": lastApplied["db_user"].(string),
  346. },
  347. })
  348. if err != nil {
  349. return fmt.Errorf("failed to create RDS env group: %s", err.Error())
  350. }
  351. return nil
  352. }
  353. func deleteRDSEnvGroup(ctx context.Context, config *config.Config, infra *models.Infra, lastApplied map[string]interface{}) error {
  354. cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
  355. if err != nil {
  356. return err
  357. }
  358. ooc := &kubernetes.OutOfClusterConfig{
  359. Repo: config.Repo,
  360. DigitalOceanOAuth: config.DOConf,
  361. Cluster: cluster,
  362. }
  363. agent, err := kubernetes.GetAgentOutOfClusterConfig(ctx, ooc)
  364. if err != nil {
  365. return fmt.Errorf("failed to get agent: %s", err.Error())
  366. }
  367. err = envgroup.DeleteEnvGroup(agent, fmt.Sprintf("rds-credentials-%s", lastApplied["db_name"].(string)), "default")
  368. if err != nil {
  369. return fmt.Errorf("failed to create RDS env group: %s", err.Error())
  370. }
  371. return nil
  372. }
  373. func createS3EnvGroup(ctx context.Context, config *config.Config, infra *models.Infra, lastApplied map[string]interface{}, output map[string]interface{}) error {
  374. cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
  375. if err != nil {
  376. return err
  377. }
  378. ooc := &kubernetes.OutOfClusterConfig{
  379. Repo: config.Repo,
  380. DigitalOceanOAuth: config.DOConf,
  381. Cluster: cluster,
  382. }
  383. agent, err := kubernetes.GetAgentOutOfClusterConfig(ctx, ooc)
  384. if err != nil {
  385. return fmt.Errorf("failed to get agent: %s", err.Error())
  386. }
  387. // split the instance endpoint on the port
  388. _, err = envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
  389. Name: fmt.Sprintf("s3-credentials-%s", lastApplied["bucket_name"].(string)),
  390. Namespace: "default",
  391. Variables: map[string]string{},
  392. SecretVariables: map[string]string{
  393. "S3_AWS_ACCESS_KEY_ID": output["s3_aws_access_key_id"].(string),
  394. "S3_AWS_SECRET_KEY": output["s3_aws_secret_key"].(string),
  395. "S3_BUCKET_NAME": output["s3_bucket_name"].(string),
  396. },
  397. })
  398. if err != nil {
  399. return fmt.Errorf("failed to create S3 env group: %s", err.Error())
  400. }
  401. return nil
  402. }
  403. func deleteS3EnvGroup(ctx context.Context, config *config.Config, infra *models.Infra, lastApplied map[string]interface{}) error {
  404. cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
  405. if err != nil {
  406. return err
  407. }
  408. ooc := &kubernetes.OutOfClusterConfig{
  409. Repo: config.Repo,
  410. DigitalOceanOAuth: config.DOConf,
  411. Cluster: cluster,
  412. }
  413. agent, err := kubernetes.GetAgentOutOfClusterConfig(ctx, ooc)
  414. if err != nil {
  415. return fmt.Errorf("failed to get agent: %s", err.Error())
  416. }
  417. err = envgroup.DeleteEnvGroup(agent, fmt.Sprintf("s3-credentials-%s", lastApplied["bucket_name"].(string)), "default")
  418. if err != nil {
  419. return fmt.Errorf("failed to create RDS env group: %s", err.Error())
  420. }
  421. return nil
  422. }