update.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. region, err := h.getClusterRegion(ctx, project.ID, cluster.ID)
  66. if err != nil {
  67. err = telemetry.Error(ctx, span, err, "error getting cluster region")
  68. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  69. return
  70. }
  71. // assume we are creating for now; will add update support later
  72. datastoreProto := &porterv1.ManagedDatastore{
  73. CloudProvider: porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS,
  74. CloudProviderCredentialIdentifier: cluster.CloudProviderCredentialIdentifier,
  75. Region: region,
  76. ConnectedClusters: &porterv1.ConnectedClusters{
  77. ConnectedClusterIds: []int64{int64(cluster.ID)},
  78. },
  79. }
  80. marshaledValues, err := json.Marshal(request.Values)
  81. if err != nil {
  82. err = telemetry.Error(ctx, span, err, "error marshaling values")
  83. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  84. return
  85. }
  86. var datastoreValues struct {
  87. Config struct {
  88. Name string `json:"name"`
  89. DatabaseName string `json:"databaseName"`
  90. MasterUsername string `json:"masterUsername"`
  91. MasterUserPassword string `json:"masterUserPassword"`
  92. AllocatedStorage int64 `json:"allocatedStorage"`
  93. InstanceClass string `json:"instanceClass"`
  94. EngineVersion string `json:"engineVersion"`
  95. } `json:"config"`
  96. }
  97. err = json.Unmarshal(marshaledValues, &datastoreValues)
  98. if err != nil {
  99. err = telemetry.Error(ctx, span, err, "error unmarshaling rds postgres values")
  100. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  101. return
  102. }
  103. if datastoreValues.Config.Name == "" {
  104. err = telemetry.Error(ctx, span, nil, "datastore name is required")
  105. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  106. return
  107. }
  108. datastoreProto.Name = datastoreValues.Config.Name
  109. switch request.Type {
  110. case "RDS":
  111. var engine porterv1.EnumAwsRdsEngine
  112. switch request.Engine {
  113. case "POSTGRES":
  114. engine = porterv1.EnumAwsRdsEngine_ENUM_AWS_RDS_ENGINE_POSTGRESQL
  115. case "AURORA-POSTGRES":
  116. engine = porterv1.EnumAwsRdsEngine_ENUM_AWS_RDS_ENGINE_AURORA_POSTGRESQL
  117. default:
  118. err = telemetry.Error(ctx, span, nil, "invalid rds engine")
  119. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  120. return
  121. }
  122. datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_AWS_RDS
  123. datastoreProto.KindValues = &porterv1.ManagedDatastore_AwsRdsKind{
  124. AwsRdsKind: &porterv1.AwsRds{
  125. DatabaseName: pointer.String(datastoreValues.Config.DatabaseName),
  126. MasterUsername: pointer.String(datastoreValues.Config.MasterUsername),
  127. MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
  128. AllocatedStorageGigabytes: pointer.Int64(datastoreValues.Config.AllocatedStorage),
  129. InstanceClass: pointer.String(datastoreValues.Config.InstanceClass),
  130. Engine: engine,
  131. EngineVersion: pointer.String(datastoreValues.Config.EngineVersion),
  132. },
  133. }
  134. case "ELASTICACHE":
  135. datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_AWS_ELASTICACHE
  136. datastoreProto.KindValues = &porterv1.ManagedDatastore_AwsElasticacheKind{
  137. AwsElasticacheKind: &porterv1.AwsElasticache{
  138. Engine: porterv1.EnumAwsElasticacheEngine_ENUM_AWS_ELASTICACHE_ENGINE_REDIS,
  139. InstanceClass: pointer.String(datastoreValues.Config.InstanceClass),
  140. MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
  141. EngineVersion: pointer.String(datastoreValues.Config.EngineVersion),
  142. },
  143. }
  144. default:
  145. err = telemetry.Error(ctx, span, nil, "invalid datastore type")
  146. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  147. return
  148. }
  149. req := connect.NewRequest(&porterv1.PatchCloudContractRequest{
  150. ProjectId: int64(project.ID),
  151. Operation: porterv1.EnumPatchCloudContractOperation_ENUM_PATCH_CLOUD_CONTRACT_OPERATION_UPDATE,
  152. ResourceType: porterv1.EnumPatchCloudContractType_ENUM_PATCH_CLOUD_CONTRACT_TYPE_DATASTORE,
  153. ResourceValues: &porterv1.PatchCloudContractRequest_Datastore{
  154. Datastore: datastoreProto,
  155. },
  156. })
  157. _, err = h.Config().ClusterControlPlaneClient.PatchCloudContract(ctx, req)
  158. if err != nil {
  159. err = telemetry.Error(ctx, span, err, "error patching cloud contract")
  160. h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  161. return
  162. }
  163. h.WriteResult(w, r, UpdateDatastoreResponse{})
  164. }
  165. // 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
  166. func (h *UpdateDatastoreHandler) getClusterRegion(
  167. ctx context.Context,
  168. projectId uint,
  169. clusterId uint,
  170. ) (string, error) {
  171. ctx, span := telemetry.NewSpan(ctx, "get-cluster-region")
  172. defer span.End()
  173. telemetry.WithAttributes(span,
  174. telemetry.AttributeKV{Key: "project-id", Value: projectId},
  175. telemetry.AttributeKV{Key: "cluster-id", Value: clusterId},
  176. )
  177. var region string
  178. var clusterContractRecord *models.APIContractRevision
  179. clusterContractRevisions, err := h.Config().Repo.APIContractRevisioner().List(ctx, projectId, repository.WithClusterID(clusterId), repository.WithLatest(true))
  180. if err != nil {
  181. return region, telemetry.Error(ctx, span, err, "error getting latest cluster contract revisions")
  182. }
  183. if len(clusterContractRevisions) == 0 {
  184. return region, telemetry.Error(ctx, span, nil, "no cluster contract revisions found")
  185. }
  186. clusterContractRecord = clusterContractRevisions[0]
  187. var clusterContractProto porterv1.Contract
  188. decoded, err := base64.StdEncoding.DecodeString(clusterContractRecord.Base64Contract)
  189. if err != nil {
  190. return region, telemetry.Error(ctx, span, err, "error decoding cluster contract")
  191. }
  192. err = helpers.UnmarshalContractObject(decoded, &clusterContractProto)
  193. if err != nil {
  194. return region, telemetry.Error(ctx, span, err, "error unmarshalling cluster contract")
  195. }
  196. clusterProto := clusterContractProto.Cluster
  197. if clusterProto == nil {
  198. return region, telemetry.Error(ctx, span, nil, "cluster contract proto is nil")
  199. }
  200. eksKindValues := clusterProto.GetEksKind()
  201. if eksKindValues == nil {
  202. return region, telemetry.Error(ctx, span, nil, "eks kind values are nil")
  203. }
  204. region = eksKindValues.Region
  205. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "region", Value: region})
  206. return region, nil
  207. }