update_notification_config.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package notifications
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "connectrpc.com/connect"
  7. "github.com/porter-dev/porter/api/server/shared/requestutils"
  8. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  9. "github.com/porter-dev/porter/api/server/shared/apierrors"
  10. "github.com/porter-dev/porter/api/types"
  11. "github.com/porter-dev/porter/internal/models"
  12. "github.com/porter-dev/porter/internal/telemetry"
  13. "github.com/porter-dev/porter/api/server/handlers"
  14. "github.com/porter-dev/porter/api/server/shared"
  15. "github.com/porter-dev/porter/api/server/shared/config"
  16. )
  17. // UpdateNotificationConfigHandler is the handler for the POST /notifications/config/{notification_config_id} endpoint
  18. type UpdateNotificationConfigHandler struct {
  19. handlers.PorterHandlerReadWriter
  20. }
  21. // NewUpdateNotificationConfigHandler returns a new UpdateNotificationConfigHandler
  22. func NewUpdateNotificationConfigHandler(
  23. config *config.Config,
  24. decoderValidator shared.RequestDecoderValidator,
  25. writer shared.ResultWriter,
  26. ) *UpdateNotificationConfigHandler {
  27. return &UpdateNotificationConfigHandler{
  28. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  29. }
  30. }
  31. // UpdateNotificationConfigRequest is the request object for the /notifications/config/{notification_config_id} endpoint
  32. type UpdateNotificationConfigRequest struct {
  33. Config Config `json:"config"`
  34. SlackIntegrationID uint `json:"slack_integration_id"`
  35. }
  36. // Config is the config object for the /notifications endpoint
  37. type Config struct {
  38. Mention string `json:"mention"`
  39. Statuses StatusesEnabled `json:"statuses"`
  40. Types TypesEnabled `json:"types"`
  41. }
  42. // StatusesEnabled is a struct that signifies whether a status is enabled or not
  43. type StatusesEnabled struct {
  44. Successful bool `json:"successful"`
  45. Failed bool `json:"failed"`
  46. Progressing bool `json:"progressing"`
  47. }
  48. // TypesEnabled is a struct that signifies whether a type is enabled or not
  49. type TypesEnabled struct {
  50. Deploy bool `json:"deploy"`
  51. Build bool `json:"build"`
  52. PreDeploy bool `json:"predeploy"`
  53. Alert bool `json:"alert"`
  54. }
  55. // UpdateNotificationConfigResponse is the response object for the /notifications/config/{notification_config_id} endpoint
  56. type UpdateNotificationConfigResponse struct {
  57. ID uint `json:"id"`
  58. }
  59. // ServeHTTP updates a notification config
  60. func (n *UpdateNotificationConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  61. ctx, span := telemetry.NewSpan(r.Context(), "serve-notification-config-update")
  62. defer span.End()
  63. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  64. telemetry.WithAttributes(span,
  65. telemetry.AttributeKV{Key: "project-id", Value: project.ID},
  66. )
  67. notificationConfigID, reqErr := requestutils.GetURLParamUint(r, types.URLParamNotificationConfigID)
  68. if reqErr != nil {
  69. e := telemetry.Error(ctx, span, nil, "error parsing event id from url")
  70. n.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
  71. return
  72. }
  73. telemetry.WithAttributes(span,
  74. telemetry.AttributeKV{Key: "notification-config-id", Value: notificationConfigID},
  75. )
  76. request := &UpdateNotificationConfigRequest{}
  77. if ok := n.DecodeAndValidate(w, r, request); !ok {
  78. err := telemetry.Error(ctx, span, nil, "error decoding request")
  79. n.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  80. return
  81. }
  82. configProto, err := configToProto(request.Config)
  83. if err != nil {
  84. err := telemetry.Error(ctx, span, err, "error converting config to proto")
  85. n.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  86. return
  87. }
  88. updateReq := connect.NewRequest(&porterv1.UpdateNotificationConfigRequest{
  89. ProjectId: int64(project.ID),
  90. NotificationConfigId: int64(notificationConfigID),
  91. Config: configProto,
  92. SlackIntegrationId: int64(request.SlackIntegrationID),
  93. })
  94. updateResp, err := n.Config().ClusterControlPlaneClient.UpdateNotificationConfig(ctx, updateReq)
  95. if err != nil {
  96. err := telemetry.Error(ctx, span, err, "error calling ccp apply porter app")
  97. n.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  98. return
  99. }
  100. if updateResp == nil || updateResp.Msg == nil {
  101. err := telemetry.Error(ctx, span, nil, "ccp response or msg is nil")
  102. n.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  103. return
  104. }
  105. response := &UpdateNotificationConfigResponse{
  106. ID: uint(updateResp.Msg.NotificationConfigId),
  107. }
  108. n.WriteResult(w, r, response)
  109. }
  110. func configToProto(config Config) (*porterv1.NotificationConfig, error) {
  111. statusMap := map[string]bool{}
  112. by, err := json.Marshal(config.Statuses)
  113. if err != nil {
  114. return nil, fmt.Errorf("error marshalling statuses: %s", err)
  115. }
  116. err = json.Unmarshal(by, &statusMap)
  117. if err != nil {
  118. return nil, fmt.Errorf("error unmarshalling statuses: %s", err)
  119. }
  120. var statuses []*porterv1.NotificationStatusEnabled
  121. for status, enabled := range statusMap {
  122. if protoStatus, ok := transformStatusStringToProto[status]; ok {
  123. statuses = append(statuses, &porterv1.NotificationStatusEnabled{
  124. Status: protoStatus,
  125. Enabled: enabled,
  126. })
  127. }
  128. }
  129. typeMap := map[string]bool{}
  130. by, err = json.Marshal(config.Types)
  131. if err != nil {
  132. return nil, fmt.Errorf("error marshalling types: %s", err)
  133. }
  134. err = json.Unmarshal(by, &typeMap)
  135. if err != nil {
  136. return nil, fmt.Errorf("error unmarshalling types: %s", err)
  137. }
  138. var types []*porterv1.NotificationTypeEnabled
  139. for t, enabled := range typeMap {
  140. if protoType, ok := transformTypeStringToProto[t]; ok {
  141. types = append(types, &porterv1.NotificationTypeEnabled{
  142. Type: protoType,
  143. Enabled: enabled,
  144. })
  145. }
  146. }
  147. protoConfig := &porterv1.NotificationConfig{
  148. EnabledStatuses: statuses,
  149. EnabledTypes: types,
  150. SlackConfig: &porterv1.SlackConfig{Mentions: []string{config.Mention}},
  151. }
  152. return protoConfig, nil
  153. }
  154. var transformStatusStringToProto = map[string]porterv1.EnumNotificationStatus{
  155. "successful": porterv1.EnumNotificationStatus_ENUM_NOTIFICATION_STATUS_SUCCESSFUL,
  156. "failed": porterv1.EnumNotificationStatus_ENUM_NOTIFICATION_STATUS_FAILED,
  157. "progressing": porterv1.EnumNotificationStatus_ENUM_NOTIFICATION_STATUS_PROGRESSING,
  158. }
  159. var transformTypeStringToProto = map[string]porterv1.EnumNotificationEventType{
  160. "deploy": porterv1.EnumNotificationEventType_ENUM_NOTIFICATION_EVENT_TYPE_DEPLOY,
  161. "build": porterv1.EnumNotificationEventType_ENUM_NOTIFICATION_EVENT_TYPE_BUILD,
  162. "predeploy": porterv1.EnumNotificationEventType_ENUM_NOTIFICATION_EVENT_TYPE_PREDEPLOY,
  163. "alert": porterv1.EnumNotificationEventType_ENUM_NOTIFICATION_EVENT_TYPE_ALERT,
  164. }
  165. // reverseMap returns a map with the keys and values swapped
  166. func reverseMap[K comparable, V comparable](m map[K]V) map[V]K {
  167. result := map[V]K{}
  168. for k, v := range m {
  169. result[v] = k
  170. }
  171. return result
  172. }
  173. // mapKeys returns the keys of a map as a slice
  174. func mapKeys[K comparable, V any](m map[K]V) []K {
  175. var keys []K
  176. for k := range m {
  177. keys = append(keys, k)
  178. }
  179. return keys
  180. }
  181. // trueMap returns a map with the keys set to true
  182. func trueMap[K comparable](keys []K) map[K]bool {
  183. m := map[K]bool{}
  184. for _, k := range keys {
  185. m[k] = true
  186. }
  187. return m
  188. }
  189. var (
  190. transformProtoToStatusString = reverseMap(transformStatusStringToProto)
  191. transformProtoToTypeString = reverseMap(transformTypeStringToProto)
  192. allStatuses = mapKeys(transformStatusStringToProto)
  193. allTypes = mapKeys(transformTypeStringToProto)
  194. )