provision_rds.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package provision
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strconv"
  8. "github.com/mitchellh/mapstructure"
  9. "github.com/porter-dev/porter/api/server/authz"
  10. "github.com/porter-dev/porter/api/server/handlers"
  11. "github.com/porter-dev/porter/api/server/shared"
  12. "github.com/porter-dev/porter/api/server/shared/apierrors"
  13. "github.com/porter-dev/porter/api/server/shared/config"
  14. "github.com/porter-dev/porter/api/types"
  15. "github.com/porter-dev/porter/ee/integrations/httpbackend"
  16. "github.com/porter-dev/porter/internal/kubernetes/provisioner"
  17. "github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/rds"
  18. "github.com/porter-dev/porter/internal/models"
  19. "github.com/porter-dev/porter/internal/repository"
  20. "gorm.io/gorm"
  21. )
  22. type ProvisionRDSHandler struct {
  23. handlers.PorterHandlerReadWriter
  24. authz.KubernetesAgentGetter
  25. }
  26. func NewProvisionRDSHandler(
  27. config *config.Config,
  28. decoderValidator shared.RequestDecoderValidator,
  29. writer shared.ResultWriter,
  30. ) *ProvisionRDSHandler {
  31. return &ProvisionRDSHandler{
  32. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  33. }
  34. }
  35. func (c *ProvisionRDSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  36. user, _ := r.Context().Value(types.UserScope).(*models.User)
  37. proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
  38. namespace := r.Context().Value(types.NamespaceScope).(string)
  39. cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
  40. request := &types.CreateRDSInfraRequest{}
  41. if ok := c.DecodeAndValidate(w, r, request); !ok {
  42. return
  43. }
  44. // validate db version and family
  45. if v, ok := types.DBVersionMapping[types.Family(request.DBFamily)]; !ok {
  46. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  47. errors.New("DB family does not exist"), http.StatusBadRequest))
  48. return
  49. } else {
  50. if !v.VersionExists(types.EngineVersion(request.DBEngineVersion)) {
  51. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  52. errors.New("DB version not available for the given family"), http.StatusBadRequest))
  53. return
  54. }
  55. }
  56. dbVersion := types.EngineVersion(request.DBEngineVersion)
  57. clusterInfra, err := c.Repo().Infra().ReadInfra(proj.ID, cluster.InfraID)
  58. if err != nil {
  59. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  60. fmt.Errorf("empty cluster infra, projectID: %d, infraID: %d", proj.ID, cluster.InfraID),
  61. http.StatusNotFound,
  62. ))
  63. return
  64. }
  65. // get the tfstate from the HTTP backend using the infra ID
  66. client := httpbackend.NewClient(c.Config().ServerConf.ProvisionerBackendURL)
  67. // get the unique infra name and query from the TF HTTP backend
  68. currentState, err := client.GetCurrentState(clusterInfra.GetUniqueName())
  69. if err != nil && errors.Is(err, httpbackend.ErrNotFound) {
  70. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  71. err,
  72. http.StatusNotFound,
  73. ))
  74. return
  75. } else if err != nil {
  76. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  77. return
  78. }
  79. var vpc, region string
  80. var subnets []string
  81. var opts *provisioner.ProvisionOpts
  82. vaultToken := ""
  83. vpc, subnets, err = c.ExtractVPCFromEKSTFState(currentState, "aws_eks_cluster.cluster")
  84. if err != nil {
  85. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  86. err,
  87. http.StatusInternalServerError,
  88. ))
  89. return
  90. }
  91. suffix, err := repository.GenerateRandomBytes(6)
  92. if err != nil {
  93. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  94. return
  95. }
  96. dbInfra := &models.Infra{
  97. ProjectID: proj.ID,
  98. Status: types.StatusCreating,
  99. Suffix: suffix,
  100. CreatedByUserID: user.ID,
  101. }
  102. switch clusterInfra.Kind {
  103. case types.InfraGKE:
  104. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  105. errors.New("unsupported cluster kind"),
  106. http.StatusBadRequest,
  107. ))
  108. return
  109. case types.InfraEKS:
  110. dbInfra.Kind = types.InfraRDS
  111. dbInfra.AWSIntegrationID = clusterInfra.AWSIntegrationID
  112. integration, err := c.Repo().AWSIntegration().ReadAWSIntegration(clusterInfra.ProjectID, clusterInfra.AWSIntegrationID)
  113. if err != nil {
  114. if err == gorm.ErrRecordNotFound {
  115. c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
  116. } else {
  117. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  118. }
  119. return
  120. }
  121. region = integration.AWSRegion
  122. if c.Config().CredentialBackend != nil {
  123. vaultToken, err = c.Config().CredentialBackend.CreateAWSToken(integration)
  124. if err != nil {
  125. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  126. }
  127. }
  128. vpc, subnets, err = c.ExtractVPCFromEKSTFState(currentState, "aws_eks_cluster.cluster")
  129. if err != nil {
  130. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  131. err,
  132. http.StatusInternalServerError,
  133. ))
  134. return
  135. }
  136. case types.InfraDOKS:
  137. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  138. errors.New("unsupported cluster kind"),
  139. http.StatusBadRequest,
  140. ))
  141. return
  142. }
  143. if len(subnets) != 3 {
  144. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  145. errors.New("Length of subnets is not 3: not a valid VPC"),
  146. http.StatusNotImplemented,
  147. ))
  148. return
  149. }
  150. lastAppliedData := &types.RDSInfraLastApplied{
  151. CreateRDSInfraRequest: request,
  152. ClusterID: cluster.ID,
  153. Namespace: namespace,
  154. AWSRegion: region,
  155. DBMajorEngineVersion: dbVersion.MajorVersion(),
  156. DBStorageEncrypted: strconv.FormatBool(request.DBEncryption),
  157. DeletionProtection: strconv.FormatBool(true),
  158. VPCID: vpc,
  159. Subnet1: subnets[0],
  160. Subnet2: subnets[1],
  161. Subnet3: subnets[2],
  162. }
  163. lastApplied, err := json.Marshal(lastAppliedData)
  164. if err != nil {
  165. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  166. return
  167. }
  168. dbInfra.LastApplied = lastApplied
  169. // handle write to the database
  170. infra, err := c.Repo().Infra().CreateInfra(dbInfra)
  171. if err != nil {
  172. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  173. return
  174. }
  175. opts, err = GetSharedProvisionerOpts(c.Config(), infra)
  176. if err != nil {
  177. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  178. return
  179. }
  180. opts.CredentialExchange.VaultToken = vaultToken
  181. opts.RDS = &rds.Conf{
  182. AWSRegion: region,
  183. DBName: request.DBName,
  184. MachineType: request.MachineType,
  185. DBEngineVersion: request.DBEngineVersion,
  186. DBFamily: request.DBFamily,
  187. DBMajorEngineVersion: dbVersion.MajorVersion(),
  188. DBAllocatedStorage: request.DBStorage,
  189. DBMaxAllocatedStorage: request.DBMaxStorage,
  190. DBStorageEncrypted: strconv.FormatBool(request.DBEncryption),
  191. Username: request.Username,
  192. Password: request.Password,
  193. VPCID: vpc,
  194. DeletionProtection: strconv.FormatBool(true),
  195. Subnet1: subnets[0],
  196. Subnet2: subnets[1],
  197. Subnet3: subnets[2],
  198. }
  199. opts.OperationKind = provisioner.Apply
  200. err = c.Config().ProvisionerAgent.Provision(opts)
  201. if err != nil {
  202. infra.Status = types.StatusError
  203. infra, _ = c.Repo().Infra().UpdateInfra(infra)
  204. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  205. return
  206. }
  207. c.WriteResult(w, r, infra.ToInfraType())
  208. }
  209. func (c *ProvisionRDSHandler) ExtractVPCFromEKSTFState(tfState *httpbackend.TFState, resourceIdentifier string) (string, []string, error) {
  210. for _, resource := range tfState.Resources {
  211. if resourceIdentifier == resource.Type+"."+resource.Name {
  212. for _, instance := range resource.Instances {
  213. vpcConfig, ok := instance.Attributes["vpc_config"]
  214. if !ok {
  215. return "", []string{}, errors.New("name not found for the requested resource name-type")
  216. }
  217. awsVPCConfigIface, ok := vpcConfig.([]interface{})
  218. if !ok {
  219. fmt.Printf("%#v\n", vpcConfig)
  220. return "", []string{}, errors.New("cannot cast returned value to vpc config")
  221. }
  222. if len(awsVPCConfigIface) == 0 {
  223. return "", []string{}, errors.New("empty vpc config")
  224. }
  225. awsVPCConfigMap, ok := awsVPCConfigIface[0].(map[string]interface{})
  226. if !ok {
  227. return "", []string{}, errors.New("cannot cast returned value to vpc config map")
  228. }
  229. var awsVPCConfig httpbackend.AWSVPCConfig
  230. err := mapstructure.Decode(awsVPCConfigMap, &awsVPCConfig)
  231. if err != nil {
  232. return "", []string{}, errors.New("cannot cast returned value to vpc config")
  233. }
  234. return awsVPCConfig.VPCID, awsVPCConfig.SubNetIDs, nil
  235. }
  236. return "", []string{}, errors.New("name not found for the requested resource name-type")
  237. // return c._extractVPCFromResourceInstance(resource, "id")
  238. }
  239. }
  240. return "", []string{}, errors.New("name not found for the requested resource name-type")
  241. }
  242. func (c *ProvisionRDSHandler) ExtractVPCFromGKETFState(tfState *httpbackend.TFState, resourceIdentifier string) (string, error) {
  243. for _, resource := range tfState.Resources {
  244. // fmt.Printf("%s.%s\n", resource.Type, resource.Name)
  245. if resourceIdentifier == resource.Type+"."+resource.Name {
  246. return c._extractVPCFromResourceInstance(resource, "name")
  247. }
  248. }
  249. return "", errors.New("name not found for the requested resource name-type")
  250. }
  251. func (c *ProvisionRDSHandler) _extractVPCFromResourceInstance(resource httpbackend.TFStateResource, attributeName string) (string, error) {
  252. for _, instance := range resource.Instances {
  253. vpc, ok := instance.Attributes[attributeName]
  254. if !ok {
  255. return "", errors.New("name not found for the requested resource name-type")
  256. }
  257. vpcName, ok := vpc.(string)
  258. if !ok {
  259. return "", errors.New("cannot cast returned value to string")
  260. }
  261. return vpcName, nil
  262. }
  263. return "", errors.New("name not found for the requested resource name-type")
  264. }
  265. func (c *ProvisionRDSHandler) _qualifyGormError(err error) apierrors.RequestError {
  266. if err == gorm.ErrRecordNotFound {
  267. return apierrors.NewErrForbidden(err)
  268. } else {
  269. return apierrors.NewErrInternal(err)
  270. }
  271. }