update.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package datastore
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "net/http"
  7. "connectrpc.com/connect"
  8. "github.com/porter-dev/api-contracts/generated/go/helpers"
  9. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  10. "github.com/porter-dev/porter/api/server/authz"
  11. "github.com/porter-dev/porter/api/server/handlers"
  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/server/shared/config"
  15. "github.com/porter-dev/porter/api/types"
  16. "github.com/porter-dev/porter/internal/models"
  17. "github.com/porter-dev/porter/internal/repository"
  18. "github.com/porter-dev/porter/internal/telemetry"
  19. "k8s.io/utils/pointer"
  20. )
  21. // UpdateDatastoreHandler is a struct for updating datastores.
  22. // Currently, this is expected to used once (on create) and then not again, however the 'update' terminology was proactively used
  23. // so we can reuse this handler when we support updates in the future.
  24. type UpdateDatastoreHandler struct {
  25. handlers.PorterHandlerReadWriter
  26. authz.KubernetesAgentGetter
  27. }
  28. // NewUpdateDatastoreHandler constructs a datastore UpdateDatastoreHandler
  29. func NewUpdateDatastoreHandler(
  30. config *config.Config,
  31. decoderValidator shared.RequestDecoderValidator,
  32. writer shared.ResultWriter,
  33. ) *UpdateDatastoreHandler {
  34. return &UpdateDatastoreHandler{
  35. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  36. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  37. }
  38. }
  39. // UpdateDatastoreRequest is the expected format of the request body
  40. type UpdateDatastoreRequest struct {
  41. Name string `json:"name"`
  42. Type string `json:"type"`
  43. Engine string `json:"engine"`
  44. Values map[string]interface{} `json:"values"`
  45. }
  46. // UpdateDatastoreResponse is the expected format of the response body
  47. type UpdateDatastoreResponse struct{}
  48. // ServeHTTP updates a datastore using the decoded values
  49. func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  50. ctx, span := telemetry.NewSpan(r.Context(), "serve-update-datastore")
  51. defer span.End()
  52. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  53. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  54. request := &UpdateDatastoreRequest{}
  55. if ok := h.DecodeAndValidate(w, r, request); !ok {
  56. err := telemetry.Error(ctx, span, nil, "error decoding update datastore request")
  57. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  58. return
  59. }
  60. telemetry.WithAttributes(span,
  61. telemetry.AttributeKV{Key: "name", Value: request.Name},
  62. telemetry.AttributeKV{Key: "type", Value: request.Type},
  63. telemetry.AttributeKV{Key: "engine", Value: request.Engine},
  64. )
  65. // assume we are creating for now; will add update support later
  66. datastoreProto := &porterv1.ManagedDatastore{
  67. ConnectedClusters: &porterv1.ConnectedClusters{
  68. ConnectedClusterIds: []int64{int64(cluster.ID)},
  69. },
  70. }
  71. marshaledValues, err := json.Marshal(request.Values)
  72. if err != nil {
  73. err = telemetry.Error(ctx, span, err, "error marshaling values")
  74. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  75. return
  76. }
  77. var datastoreValues struct {
  78. Config struct {
  79. Name string `json:"name"`
  80. DatabaseName string `json:"databaseName"`
  81. MasterUsername string `json:"masterUsername"`
  82. MasterUserPassword string `json:"masterUserPassword"`
  83. AllocatedStorage int64 `json:"allocatedStorage"`
  84. InstanceClass string `json:"instanceClass"`
  85. EngineVersion string `json:"engineVersion"`
  86. CpuCores float32 `json:"cpuCores"`
  87. RamMegabytes int `json:"ramMegabytes"`
  88. } `json:"config"`
  89. }
  90. err = json.Unmarshal(marshaledValues, &datastoreValues)
  91. if err != nil {
  92. err = telemetry.Error(ctx, span, err, "error unmarshaling rds postgres values")
  93. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  94. return
  95. }
  96. if datastoreValues.Config.Name == "" {
  97. err = telemetry.Error(ctx, span, nil, "datastore name is required")
  98. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  99. return
  100. }
  101. datastoreProto.Name = datastoreValues.Config.Name
  102. switch request.Type {
  103. case "RDS":
  104. var engine porterv1.EnumAwsRdsEngine
  105. switch request.Engine {
  106. case "POSTGRES":
  107. engine = porterv1.EnumAwsRdsEngine_ENUM_AWS_RDS_ENGINE_POSTGRESQL
  108. case "AURORA-POSTGRES":
  109. engine = porterv1.EnumAwsRdsEngine_ENUM_AWS_RDS_ENGINE_AURORA_POSTGRESQL
  110. default:
  111. err = telemetry.Error(ctx, span, nil, "invalid rds engine")
  112. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  113. return
  114. }
  115. region, err := h.getClusterRegion(ctx, project.ID, cluster.ID)
  116. if err != nil {
  117. err = telemetry.Error(ctx, span, err, "error getting cluster region")
  118. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  119. return
  120. }
  121. datastoreProto.Region = region
  122. datastoreProto.CloudProvider = porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS
  123. datastoreProto.CloudProviderCredentialIdentifier = cluster.CloudProviderCredentialIdentifier
  124. datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_AWS_RDS
  125. datastoreProto.KindValues = &porterv1.ManagedDatastore_AwsRdsKind{
  126. AwsRdsKind: &porterv1.AwsRds{
  127. DatabaseName: pointer.String(datastoreValues.Config.DatabaseName),
  128. MasterUsername: pointer.String(datastoreValues.Config.MasterUsername),
  129. MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
  130. AllocatedStorageGigabytes: pointer.Int64(datastoreValues.Config.AllocatedStorage),
  131. InstanceClass: pointer.String(datastoreValues.Config.InstanceClass),
  132. Engine: engine,
  133. EngineVersion: pointer.String(datastoreValues.Config.EngineVersion),
  134. },
  135. }
  136. case "ELASTICACHE":
  137. region, err := h.getClusterRegion(ctx, project.ID, cluster.ID)
  138. if err != nil {
  139. err = telemetry.Error(ctx, span, err, "error getting cluster region")
  140. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  141. return
  142. }
  143. datastoreProto.Region = region
  144. datastoreProto.CloudProvider = porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS
  145. datastoreProto.CloudProviderCredentialIdentifier = cluster.CloudProviderCredentialIdentifier
  146. datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_AWS_ELASTICACHE
  147. datastoreProto.KindValues = &porterv1.ManagedDatastore_AwsElasticacheKind{
  148. AwsElasticacheKind: &porterv1.AwsElasticache{
  149. Engine: porterv1.EnumAwsElasticacheEngine_ENUM_AWS_ELASTICACHE_ENGINE_REDIS,
  150. InstanceClass: pointer.String(datastoreValues.Config.InstanceClass),
  151. MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
  152. EngineVersion: pointer.String(datastoreValues.Config.EngineVersion),
  153. },
  154. }
  155. case "MANAGED-POSTGRES":
  156. datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_MANAGED_POSTGRES
  157. datastoreProto.KindValues = &porterv1.ManagedDatastore_ManagedPostgresKind{
  158. ManagedPostgresKind: &porterv1.Postgres{
  159. CpuCores: datastoreValues.Config.CpuCores,
  160. RamMegabytes: int32(datastoreValues.Config.RamMegabytes),
  161. StorageGigabytes: int32(datastoreValues.Config.AllocatedStorage),
  162. MasterUsername: pointer.String(datastoreValues.Config.MasterUsername),
  163. MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
  164. },
  165. }
  166. case "MANAGED-REDIS":
  167. datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_MANAGED_REDIS
  168. datastoreProto.KindValues = &porterv1.ManagedDatastore_ManagedRedisKind{
  169. ManagedRedisKind: &porterv1.Redis{
  170. CpuCores: datastoreValues.Config.CpuCores,
  171. RamMegabytes: int32(datastoreValues.Config.RamMegabytes),
  172. StorageGigabytes: int32(datastoreValues.Config.AllocatedStorage),
  173. MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
  174. },
  175. }
  176. default:
  177. err = telemetry.Error(ctx, span, nil, "invalid datastore type")
  178. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  179. return
  180. }
  181. req := connect.NewRequest(&porterv1.UpdateDatastoreRequest{
  182. ProjectId: int64(project.ID),
  183. Datastore: datastoreProto,
  184. })
  185. _, err = h.Config().ClusterControlPlaneClient.UpdateDatastore(ctx, req)
  186. if err != nil {
  187. err = telemetry.Error(ctx, span, err, "error updating datastore")
  188. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  189. return
  190. }
  191. h.WriteResult(w, r, UpdateDatastoreResponse{})
  192. }
  193. // getClusterRegion is a very hacky way of getting the region of the cluster; this will be replaced once we allow the user to specify region from the frontend
  194. func (h *UpdateDatastoreHandler) getClusterRegion(
  195. ctx context.Context,
  196. projectId uint,
  197. clusterId uint,
  198. ) (string, error) {
  199. ctx, span := telemetry.NewSpan(ctx, "get-cluster-region")
  200. defer span.End()
  201. telemetry.WithAttributes(span,
  202. telemetry.AttributeKV{Key: "project-id", Value: projectId},
  203. telemetry.AttributeKV{Key: "cluster-id", Value: clusterId},
  204. )
  205. var region string
  206. var clusterContractRecord *models.APIContractRevision
  207. clusterContractRevisions, err := h.Config().Repo.APIContractRevisioner().List(ctx, projectId, repository.WithClusterID(clusterId), repository.WithLatest(true))
  208. if err != nil {
  209. return region, telemetry.Error(ctx, span, err, "error getting latest cluster contract revisions")
  210. }
  211. if len(clusterContractRevisions) == 0 {
  212. return region, telemetry.Error(ctx, span, nil, "no cluster contract revisions found")
  213. }
  214. clusterContractRecord = clusterContractRevisions[0]
  215. var clusterContractProto porterv1.Contract
  216. decoded, err := base64.StdEncoding.DecodeString(clusterContractRecord.Base64Contract)
  217. if err != nil {
  218. return region, telemetry.Error(ctx, span, err, "error decoding cluster contract")
  219. }
  220. err = helpers.UnmarshalContractObject(decoded, &clusterContractProto)
  221. if err != nil {
  222. return region, telemetry.Error(ctx, span, err, "error unmarshalling cluster contract")
  223. }
  224. clusterProto := clusterContractProto.Cluster
  225. if clusterProto == nil {
  226. return region, telemetry.Error(ctx, span, nil, "cluster contract proto is nil")
  227. }
  228. eksKindValues := clusterProto.GetEksKind()
  229. if eksKindValues == nil {
  230. return region, telemetry.Error(ctx, span, nil, "eks kind values are nil")
  231. }
  232. region = eksKindValues.Region
  233. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "region", Value: region})
  234. return region, nil
  235. }