diff --git a/cmd/server/config.go b/cmd/server/config.go index 888f35a9..aaf6b563 100644 --- a/cmd/server/config.go +++ b/cmd/server/config.go @@ -48,11 +48,10 @@ type ( } Slack struct { - SlackClientID string `split_words:"true"` - SlackClientSecret string `split_words:"true"` - SlackSigningSecret string `split_words:"true"` - SlackUserScopes []string `split_words:"true" default:""` - SlackBotScopes []string `split_words:"true" default:"commands,chat:write"` + SlackClientID string `split_words:"true"` + SlackClientSecret string `split_words:"true"` + SlackUserScopes []string `split_words:"true" default:""` + SlackBotScopes []string `split_words:"true" default:"commands,chat:write"` } Webhook struct { @@ -88,7 +87,7 @@ func (c *Config) isGithubEnabled() bool { } func (c *Config) isSlackEnabled() bool { - return c.SlackClientID != "" && c.SlackClientSecret != "" && c.SlackSigningSecret != "" + return c.SlackClientID != "" && c.SlackClientSecret != "" } func (c *Config) hasTLS() bool { diff --git a/cmd/server/main.go b/cmd/server/main.go index 2257c9bd..c9feb525 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -123,7 +123,6 @@ func newChatConfig(c *Config) *server.ChatConfig { Type: server.ChatTypeSlack, ClientID: c.SlackClientID, ClientSecret: c.SlackClientSecret, - Secret: c.SlackSigningSecret, BotScopes: c.SlackBotScopes, UserScopes: c.SlackUserScopes, } diff --git a/docs/concepts/chatops.md b/docs/concepts/chatops.md deleted file mode 100644 index 5687998e..00000000 --- a/docs/concepts/chatops.md +++ /dev/null @@ -1,7 +0,0 @@ -# Chatops - -ChatOps is a term that’s been coined recently to refer to using chat services, such as Slack or Microsoft Teams, to deploy or run some operations. Gitploy also supports Chatops as a helper tool. - -## Integration - -Currently, Gitploy supports Slack only, but it will support Microsoft Teams near the corner. You can check [the documentation](../tasks/integration.md) for details. diff --git a/docs/concepts/notification.md b/docs/concepts/notification.md new file mode 100644 index 00000000..aa09d51e --- /dev/null +++ b/docs/concepts/notification.md @@ -0,0 +1,18 @@ +# Notification + +Gitploy notifies users when deployments and reviews are created or updated. Now, Gitploy supports browser notification and Slack. + +## Browser + +Gitploy provides the browser notification to alert events as default. Almost modern browsers provide [notification API](https://developer.mozilla.org/ko/docs/Web/API/notification), but unfortunately, some browsers are not supported. To avoid browser compatibility, you can replace it with Slack. + +*Note that if the notification doesn't work even though your browser provides it, you should check the setting that it is enabled or not.* + + +## Slack + +Slack is a popular messaging app for businesses globally. Gitploy supports Slack to notify events of deployments and reviews. You can figure out the Connect button on the settings page after integrating with Slack. Check [the documentation](../tasks/integration.md) for details. + +Figure) Slack notification + +![Slack Notification](../images/slack-notification.png) \ No newline at end of file diff --git a/docs/images/slack-bot-token-scopes.png b/docs/images/slack-bot-token-scopes.png index 9aeea3b0..9ee1ca7d 100644 Binary files a/docs/images/slack-bot-token-scopes.png and b/docs/images/slack-bot-token-scopes.png differ diff --git a/docs/images/slack-deploy.png b/docs/images/slack-deploy.png deleted file mode 100644 index 6647a6a3..00000000 Binary files a/docs/images/slack-deploy.png and /dev/null differ diff --git a/docs/images/slack-interactivity.png b/docs/images/slack-interactivity.png deleted file mode 100644 index c8e99c88..00000000 Binary files a/docs/images/slack-interactivity.png and /dev/null differ diff --git a/docs/images/slack-new-command.png b/docs/images/slack-new-command.png deleted file mode 100644 index 8d78cbf9..00000000 Binary files a/docs/images/slack-new-command.png and /dev/null differ diff --git a/docs/images/slack-notification.png b/docs/images/slack-notification.png new file mode 100644 index 00000000..83fc7668 Binary files /dev/null and b/docs/images/slack-notification.png differ diff --git a/docs/references/GITPLOY_SLACK_SIGNING_SECRET.md b/docs/references/GITPLOY_SLACK_SIGNING_SECRET.md deleted file mode 100644 index 1349ddd5..00000000 --- a/docs/references/GITPLOY_SLACK_SIGNING_SECRET.md +++ /dev/null @@ -1,7 +0,0 @@ -# GITPLOY_SLACK_SIGNING_SECRET - -Optional string value configure Slack signing secret. It confirms that each request comes from Slack by verifying its unique signature. - -``` -GITPLOY_SLACK_SIGNING_SECRET=525e5a41c7cfdf2d915ad75c74d482 -``` \ No newline at end of file diff --git a/docs/references/configurations.md b/docs/references/configurations.md index 96a2487a..e72b5e6f 100644 --- a/docs/references/configurations.md +++ b/docs/references/configurations.md @@ -18,7 +18,6 @@ Index of server configuration settings: * [GITPLOY_SERVER_PROTO](./GITPLOY_SERVER_PROTO.md) * [GITPLOY_SLACK_CLIENT_ID](./GITPLOY_SLACK_CLIENT_ID.md) * [GITPLOY_SLACK_CLIENT_SECRET](./GITPLOY_SLACK_CLIENT_SECRET.md) -* [GITPLOY_SLACK_SIGNING_SECRET](./GITPLOY_SLACK_SIGNING_SECRET.md) * [GITPLOY_STORE_DRIVER](./GITPLOY_STORE_DRIVER.md) * [GITPLOY_STORE_SOURCE](./GITPLOY_STORE_SOURCE.md) * [GITPLOY_TLS_CERT](./GITPLOY_TLS_CERT.md) diff --git a/docs/tasks/integration.md b/docs/tasks/integration.md index b0c3b58b..466eddd5 100644 --- a/docs/tasks/integration.md +++ b/docs/tasks/integration.md @@ -33,44 +33,20 @@ jobs: ## Slack -Slack integration provides Chatops (i.e. deploy, rollback) and notification alert for events. +Slack integration provides notifications for events. ### Step 1: Create App -Firstly, we have to create [Slack App](https://api.slack.com/apps). Let’s click the Create App button and fill out inputs. +Firstly, we have to create [Slack App](https://api.slack.com/apps). You should click the Create App button and fill out inputs. ### Step 2: Configure Permissions -After creating App let’s move to the *OAuth & Permissions* page. On this page, we have to set up *the redirect URLs* and *Bot Token scopes*. Firstly, let’s add a new redirect URL with the `GITPLOY_SERVER_PROTO://GITPLOY_SERVER_HOST/slack/signin` format; secondly, add `chat:write` and `commands` scopes into the Bot Token scopes. +After creating App, we move to the *OAuth & Permissions* page and set up *the redirect URLs* and *Bot Token scopes*on this page. Firstly, you should add a new redirect URL with the `GITPLOY_SERVER_PROTO://GITPLOY_SERVER_HOST/slack/signin` format; secondly, add `chat:write` scope into the Bot Token scopes. Figure) Slack Bot Token Scopes ![Slack Bot Token Sceops](../images/slack-bot-token-scopes.png) -### Step 3: Create Slash Command - -To use the slash command, we have to create a new command, `/gitploy`. Move to the *Slash Commands* page, and fill out the "Create New Command" form like the following: - -* Command: `/gitploy` -* Request URL: `GITPLOY_SERVER_PROTO://GITPLOY_SERVER_HOST/slack/command` -* Short Description: `Gitploy command` -* Use Hint: `[deploy | rollback | help]` - -Figure) Slack Create New Command - -![Slack New Command](../images/slack-new-command.png) - -### Step 4: Configure Interactivity - -To enable the interactivity, we have to configure which URL interact with Slack. Move to the *Interactivity & Shortcuts* page, and fill out the "Request URL" with the `GITPLOY_SERVER_PROTO://GITPLOY_SERVER_HOST/slack/interact` - -Figure) Slack Interactivity - -![Slack Interactivity](../images/slack-interactivity.png) - -### Step 5: Run Server With App Credentials - -To enable Slack integration, you have to set up these environments when you run the server: `GITPLOY_SLACK_CLIENT_ID`, `GITPLOY_SLACK_CLIENT_SECRET`, and `GITPLOY_SLACK_SIGNING_SECRET`. You can get these credentials from *App Credentials* section of *Basic Information* page. - -On settings page, you can find the button to connect with Slack. Now, you can run the slash command `/gitploy` in Slack. +### Step 3: Run Server With App Credentials +To enable Slack integration, you have to set up these environments when you run the server: `GITPLOY_SLACK_CLIENT_ID` and `GITPLOY_SLACK_CLIENT_SECRET`. You can get these credentials from *App Credentials* section of *Basic Information* page. diff --git a/internal/server/router.go b/internal/server/router.go index 8cacdad3..32f61188 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -63,7 +63,6 @@ type ( Type ChatType ClientID string ClientSecret string - Secret string BotScopes []string UserScopes []string } @@ -219,10 +218,6 @@ func NewRouter(c *RouterConfig) *gin.Engine { if isSlackEnabled(c) { slackapi := r.Group("/slack") { - m := s.NewSlackMiddleware(&s.SlackMiddlewareConfig{ - Interactor: c.Interactor, - Secret: c.ChatConfig.Secret, - }) slack := s.NewSlack(&s.SlackConfig{ ServerHost: c.Host, ServerProto: c.Proto, @@ -232,8 +227,6 @@ func NewRouter(c *RouterConfig) *gin.Engine { slackapi.GET("", slack.Index) slackapi.GET("/signin", slack.Signin) slackapi.GET("/signout", slack.Signout) - slackapi.POST("/interact", m.Verify(), m.ParseIntr(), m.SetChatUser(), slack.Interact) - slackapi.POST("/command", m.Verify(), m.ParseCmd(), m.SetChatUser(), slack.Cmd) } } diff --git a/internal/server/slack/deploy.go b/internal/server/slack/deploy.go deleted file mode 100644 index 93bb5068..00000000 --- a/internal/server/slack/deploy.go +++ /dev/null @@ -1,383 +0,0 @@ -package slack - -import ( - "context" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - "github.com/slack-go/slack" - "go.uber.org/zap" - - "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/ent/callback" - "github.com/gitploy-io/gitploy/model/ent/deployment" - "github.com/gitploy-io/gitploy/model/ent/event" - "github.com/gitploy-io/gitploy/model/extent" - "github.com/gitploy-io/gitploy/pkg/e" -) - -const ( -// linkUnprocessalbeEntity = "https://github.com/gitploy-io/gitploy/discussions/64" -) - -const ( - // When creating a view, set unique block_ids for all blocks - // and unique action_ids for each block element. - blockEnv = "block_env" - blockType = "block_type" - blockRef = "block_ref" - blockApprovers = "block_approvers" - actionEnv = "action_env" - actionType = "action_type" - actionRef = "action_ref" - actionApprovers = "action_approver_ids" -) - -type ( - deployViewSubmission struct { - Env string - Type string - Ref string - ApproverIDs []string - } - - ErrorsPayload struct { - ResponseAction string `json:"response_action"` - Errors map[string]string `json:"errors"` - } -) - -// handleDeployCmd handles deploy command: "/gitploy deploy OWNER/REPO". -// It opens a dialog with fields to submit the payload for deployment. -func (s *Slack) handleDeployCmd(c *gin.Context) { - ctx := c.Request.Context() - - av, _ := c.Get(KeyCmd) - cmd := av.(slack.SlashCommand) - - bv, _ := c.Get(KeyChatUser) - cu := bv.(*ent.ChatUser) - - s.log.Debug("Processing deploy command.", zap.String("command", cmd.Text)) - ns, n := parseCmd(cmd.Text) - - r, err := s.i.FindRepoOfUserByNamespaceName(ctx, cu.Edges.User, ns, n) - if ent.IsNotFound(err) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, fmt.Sprintf("The `%s/%s` repository is not found.", ns, n)) - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to get the repo.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - config, err := s.i.GetConfig(ctx, cu.Edges.User, r) - if err != nil { - postResponseWithError(cmd.ChannelID, cmd.ResponseURL, err) - c.Status(http.StatusOK) - return - } - - perms, err := s.i.ListPermsOfRepo(ctx, r, "", 1, 100) - if err != nil { - s.log.Error("It has failed to list permissions.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - perms = s.filterPerms(perms, cu) - - // Create a new callback to interact with submissions. - cb, err := s.i.CreateCallback(ctx, &ent.Callback{ - Type: callback.TypeDeploy, - RepoID: r.ID, - }) - if err != nil { - s.log.Error("It has failed to create a new callback.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - _, err = slack.New(cu.BotToken). - OpenViewContext(ctx, cmd.TriggerID, buildDeployView(cb.Hash, config, perms)) - if err != nil { - s.log.Error("It has failed to open a new view.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - c.Status(http.StatusOK) -} - -func parseCmd(cmd string) (string, string) { - words := strings.Fields(cmd) - - nn := strings.Split(words[1], "/") - - return nn[0], nn[1] -} - -// filterPerms returns permissions except the deployer. -func (s *Slack) filterPerms(perms []*ent.Perm, cu *ent.ChatUser) []*ent.Perm { - ret := []*ent.Perm{} - - for _, p := range perms { - if p.Edges.User == nil { - s.log.Warn("The user edge of perm is not found.", zap.Int("perm_id", p.ID)) - continue - } - - if cu.Edges.User == nil { - s.log.Warn("The user edge of chat-user is not found.", zap.String("chat_user_id", cu.ID)) - continue - } - - if p.Edges.User.ID != cu.Edges.User.ID { - ret = append(ret, p) - } - } - - return ret -} - -func buildDeployView(callbackID string, c *extent.Config, perms []*ent.Perm) slack.ModalViewRequest { - envs := []*slack.OptionBlockObject{} - for _, env := range c.Envs { - envs = append(envs, &slack.OptionBlockObject{ - Text: &slack.TextBlockObject{ - Type: slack.PlainTextType, - Text: env.Name, - }, - Value: env.Name, - }) - } - - approvers := []*slack.OptionBlockObject{} - for _, perm := range perms { - u := perm.Edges.User - if u == nil { - continue - } - - approvers = append(approvers, &slack.OptionBlockObject{ - Text: &slack.TextBlockObject{ - Type: slack.PlainTextType, - Text: u.Login, - }, - Value: strconv.FormatInt(u.ID, 10), - }) - } - - set := []slack.Block{ - slack.NewInputBlock( - blockEnv, - slack.NewTextBlockObject(slack.PlainTextType, "Environment", false, false), - slack.NewOptionsSelectBlockElement( - slack.OptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select target environment", false, false), - actionEnv, - envs..., - ), - ), - slack.NewInputBlock( - blockType, - slack.NewTextBlockObject(slack.PlainTextType, "Type", false, false), - slack.NewOptionsSelectBlockElement( - slack.OptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select your ref type", false, false), - actionType, - slack.NewOptionBlockObject( - "commit", - slack.NewTextBlockObject(slack.PlainTextType, "Commit", false, false), - nil, - ), - slack.NewOptionBlockObject( - "branch", - slack.NewTextBlockObject(slack.PlainTextType, "Branch", false, false), - nil, - ), - slack.NewOptionBlockObject( - "tag", - slack.NewTextBlockObject(slack.PlainTextType, "Tag", false, false), - nil, - ), - ), - ), - slack.NewInputBlock( - blockRef, - slack.NewTextBlockObject(slack.PlainTextType, "Ref", false, false), - slack.NewPlainTextInputBlockElement( - slack.NewTextBlockObject(slack.PlainTextType, "E.g. Commit - 25a667d6, Branch - main, Tag - v0.1.2", false, false), - actionRef, - ), - ), - } - - if len(approvers) > 0 { - set = append(set, slack.InputBlock{ - Type: slack.MBTInput, - BlockID: blockApprovers, - Label: slack.NewTextBlockObject(slack.PlainTextType, "Approvers", false, false), - Optional: true, - Element: slack.NewOptionsSelectBlockElement( - slack.MultiOptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select approvers", false, false), - actionApprovers, - approvers..., - ), - }) - } - - return slack.ModalViewRequest{ - Type: slack.VTModal, - CallbackID: callbackID, - Title: slack.NewTextBlockObject(slack.PlainTextType, "Deploy", false, false), - Submit: slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false), - Close: slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false), - Blocks: slack.Blocks{ - BlockSet: set, - }, - } -} - -// interactDeploy deploy with the submitted payload. -func (s *Slack) interactDeploy(c *gin.Context) { - ctx := c.Request.Context() - - iv, _ := c.Get(KeyIntr) - itr := iv.(slack.InteractionCallback) - - cv, _ := c.Get(KeyChatUser) - cu := cv.(*ent.ChatUser) - - cb, _ := s.i.FindCallbackByHash(ctx, itr.View.CallbackID) - - // Parse view submission, and - // validate values. - sm := parseViewSubmissions(itr) - - // Validate the entity is processible. - _, err := s.getCommitSha(ctx, cu.Edges.User, cb.Edges.Repo, sm.Type, sm.Ref) - if e.HasErrorCode(err, e.ErrorCodeEntityNotFound) { - c.JSON(http.StatusOK, buildErrorsPayload(map[string]string{ - blockRef: "The reference is not found.", - })) - return - } else if err != nil { - s.log.Error("It has failed to get the SHA of commit.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - config, err := s.i.GetConfig(ctx, cu.Edges.User, cb.Edges.Repo) - if err != nil { - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - if err := config.Eval(&extent.EvalValues{}); err != nil { - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - var env *extent.Env - if env = config.GetEnv(sm.Env); env == nil { - postBotMessage(cu, "The env is not defined in the config.") - c.Status(http.StatusOK) - return - } - - d, err := s.i.Deploy(ctx, cu.Edges.User, cb.Edges.Repo, - &ent.Deployment{ - Type: deployment.Type(sm.Type), - Env: sm.Env, - Ref: sm.Ref, - }, - env, - ) - if err != nil { - s.log.Error("It has failed to deploy.", zap.Error(err)) - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - if _, err := s.i.CreateEvent(ctx, &ent.Event{ - Kind: event.KindDeployment, - Type: event.TypeCreated, - DeploymentID: d.ID, - }); err != nil { - s.log.Error("It has failed to create the event.", zap.Error(err)) - } - - c.Status(http.StatusOK) -} - -func parseViewSubmissions(itr slack.InteractionCallback) *deployViewSubmission { - sm := &deployViewSubmission{} - - values := itr.View.State.Values - if v, ok := values[blockEnv][actionEnv]; ok { - sm.Env = v.SelectedOption.Value - } - - if v, ok := values[blockType][actionType]; ok { - sm.Type = v.SelectedOption.Value - } - - if v, ok := values[blockRef][actionRef]; ok { - sm.Ref = v.Value - } - - ids := make([]string, 0) - if v, ok := values[blockApprovers][actionApprovers]; ok { - for _, option := range v.SelectedOptions { - ids = append(ids, option.Value) - } - - sm.ApproverIDs = ids - } - - return sm -} - -func buildErrorsPayload(errors map[string]string) *ErrorsPayload { - return &ErrorsPayload{ - ResponseAction: "errors", - Errors: errors, - } -} - -func (s *Slack) getCommitSha(ctx context.Context, u *ent.User, re *ent.Repo, typ, ref string) (string, error) { - switch typ { - case "commit": - c, err := s.i.GetCommit(ctx, u, re, ref) - if err != nil { - return "", err - } - - return c.SHA, nil - case "branch": - b, err := s.i.GetBranch(ctx, u, re, ref) - if err != nil { - return "", err - } - - return b.CommitSHA, nil - case "tag": - t, err := s.i.GetTag(ctx, u, re, ref) - if err != nil { - return "", err - } - - return t.CommitSHA, nil - default: - return "", fmt.Errorf("Type must be one of commit, branch, tag.") - } -} diff --git a/internal/server/slack/deploy_test.go b/internal/server/slack/deploy_test.go deleted file mode 100644 index c68d1a30..00000000 --- a/internal/server/slack/deploy_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package slack - -import ( - "context" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/golang/mock/gomock" - "github.com/slack-go/slack" - "go.uber.org/zap" - - "github.com/gitploy-io/gitploy/internal/server/slack/mock" - "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/ent/deployment" - "github.com/gitploy-io/gitploy/model/extent" -) - -func TestSlack_interactDeploy(t *testing.T) { - t.Run("Create a new deployment with payload.", func(t *testing.T) { - m := mock.NewMockInteractor(gomock.NewController(t)) - - // These values are in "./testdata/deploy-interact.json" - const ( - callbackID = "nafyVuEqzcchuVmV" - branch = "main" - env = "prod" - ) - - t.Log("Find the callback which was stored by the Slash command.") - m. - EXPECT(). - FindCallbackByHash(gomock.Any(), callbackID). - Return(&ent.Callback{ - Edges: ent.CallbackEdges{ - Repo: &ent.Repo{ID: 1}, - }, - }, nil) - - t.Log("Get branch to validate the payload.") - m. - EXPECT(). - GetBranch(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{}), branch). - Return(&extent.Branch{ - Name: branch, - }, nil) - - t.Log("Get the config file of the repository.") - m. - EXPECT(). - GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})). - Return(&extent.Config{ - Envs: []*extent.Env{ - {Name: env}, - }, - }, nil) - - t.Log("Deploy with the payload.") - m. - EXPECT(). - Deploy(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{}), &ent.Deployment{ - Type: deployment.TypeBranch, - Ref: branch, - Env: env, - }, gomock.AssignableToTypeOf(&extent.Env{})). - DoAndReturn(func(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, e *extent.Env) (*ent.Deployment, error) { - return d, nil - }) - - t.Log("Create a new event") - m. - EXPECT(). - CreateEvent(gomock.Any(), gomock.AssignableToTypeOf(&ent.Event{})). - Return(&ent.Event{}, nil) - - s := &Slack{ - i: m, - log: zap.L(), - } - - gin.SetMode(gin.ReleaseMode) - router := gin.New() - router.POST("/interact", func(c *gin.Context) { - bytes, _ := ioutil.ReadFile("./testdata/deploy-interact.json") - intr := slack.InteractionCallback{} - intr.UnmarshalJSON(bytes) - c.Set(KeyIntr, intr) - c.Set(KeyChatUser, &ent.ChatUser{ - Edges: ent.ChatUserEdges{ - User: &ent.User{}, - }, - }) - }, s.interactDeploy) - - req, _ := http.NewRequest("POST", "/interact", nil) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("w.Code = %d, wanted %d. Body = %v", w.Code, http.StatusOK, w.Body) - } - }) -} diff --git a/internal/server/slack/helper.go b/internal/server/slack/helper.go deleted file mode 100644 index 09814d34..00000000 --- a/internal/server/slack/helper.go +++ /dev/null @@ -1,70 +0,0 @@ -package slack - -import ( - "strconv" - - "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/pkg/e" - "github.com/slack-go/slack" -) - -func postResponseMessage(channelID, responseURL, message string) error { - _, _, _, err := slack. - New(""). - SendMessage( - channelID, - slack.MsgOptionResponseURL(responseURL, "ephemeral"), - slack.MsgOptionText(message, false), - ) - return err -} - -func postBotMessage(cu *ent.ChatUser, message string) error { - _, _, _, err := slack. - New(cu.BotToken). - SendMessage( - cu.ID, - slack.MsgOptionText(message, false), - ) - return err -} - -func postResponseWithError(channelID, responseURL string, err error) error { - var message string - if ge, ok := err.(*e.Error); ok { - message = ge.Message - } else { - message = err.Error() - } - - _, _, _, err = slack. - New(""). - SendMessage( - channelID, - slack.MsgOptionResponseURL(responseURL, "ephemeral"), - slack.MsgOptionText(message, false), - ) - return err -} - -func postMessageWithError(cu *ent.ChatUser, err error) error { - var message string - if ge, ok := err.(*e.Error); ok { - message = ge.Message - } else { - message = err.Error() - } - - _, _, _, err = slack. - New(cu.BotToken). - SendMessage( - cu.ID, - slack.MsgOptionText(message, false), - ) - return err -} - -func atoi(a string) int { - i, _ := strconv.Atoi(a) - return i -} diff --git a/internal/server/slack/interface.go b/internal/server/slack/interface.go index 84fd4e60..545a5c95 100644 --- a/internal/server/slack/interface.go +++ b/internal/server/slack/interface.go @@ -6,7 +6,6 @@ import ( "context" "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/extent" ) type ( @@ -18,35 +17,7 @@ type ( UpdateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) DeleteChatUser(ctx context.Context, cu *ent.ChatUser) error - ListPermsOfRepo(ctx context.Context, r *ent.Repo, q string, page, perPage int) ([]*ent.Perm, error) - FindPermOfRepo(ctx context.Context, r *ent.Repo, u *ent.User) (*ent.Perm, error) - - FindRepoOfUserByNamespaceName(ctx context.Context, u *ent.User, namespace, name string) (*ent.Repo, error) - UpdateRepo(ctx context.Context, r *ent.Repo) (*ent.Repo, error) - - CreateCallback(ctx context.Context, cb *ent.Callback) (*ent.Callback, error) - FindCallbackByHash(ctx context.Context, hash string) (*ent.Callback, error) - - ListDeploymentsOfRepo(ctx context.Context, r *ent.Repo, env string, status string, page, perPage int) ([]*ent.Deployment, error) - FindDeploymentByID(ctx context.Context, id int) (*ent.Deployment, error) - Deploy(ctx context.Context, u *ent.User, re *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) - GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*extent.Config, error) - - ListLocksOfRepo(ctx context.Context, r *ent.Repo) ([]*ent.Lock, error) - FindLockOfRepoByEnv(ctx context.Context, r *ent.Repo, env string) (*ent.Lock, error) - HasLockOfRepoForEnv(ctx context.Context, r *ent.Repo, env string) (bool, error) - FindLockByID(ctx context.Context, id int) (*ent.Lock, error) - CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) - DeleteLock(ctx context.Context, l *ent.Lock) error - SubscribeEvent(fn func(e *ent.Event)) error UnsubscribeEvent(fn func(e *ent.Event)) error - - CheckNotificationRecordOfEvent(ctx context.Context, e *ent.Event) bool - CreateEvent(ctx context.Context, e *ent.Event) (*ent.Event, error) - - GetCommit(ctx context.Context, u *ent.User, r *ent.Repo, sha string) (*extent.Commit, error) - GetBranch(ctx context.Context, u *ent.User, r *ent.Repo, branch string) (*extent.Branch, error) - GetTag(ctx context.Context, u *ent.User, r *ent.Repo, tag string) (*extent.Tag, error) } ) diff --git a/internal/server/slack/lock.go b/internal/server/slack/lock.go deleted file mode 100644 index bc6f243e..00000000 --- a/internal/server/slack/lock.go +++ /dev/null @@ -1,323 +0,0 @@ -package slack - -import ( - "fmt" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/nleeper/goment" - "github.com/slack-go/slack" - "go.uber.org/zap" - - "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/ent/callback" - "github.com/gitploy-io/gitploy/model/ent/perm" - "github.com/gitploy-io/gitploy/model/extent" -) - -type ( - lockViewSubmission struct { - Env string - } -) - -func (s *Slack) handleLockCmd(c *gin.Context) { - ctx := c.Request.Context() - - av, _ := c.Get(KeyCmd) - cmd := av.(slack.SlashCommand) - - bv, _ := c.Get(KeyChatUser) - cu := bv.(*ent.ChatUser) - - s.log.Debug("Processing lock command.", zap.String("command", cmd.Text)) - ns, n := parseCmd(cmd.Text) - - r, err := s.i.FindRepoOfUserByNamespaceName(ctx, cu.Edges.User, ns, n) - if ent.IsNotFound(err) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, fmt.Sprintf("The `%s/%s` repository is not found.", ns, n)) - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to get the repo.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - // Validate the perm for the repo. - if p, err := s.i.FindPermOfRepo(ctx, r, cu.Edges.User); !(p.RepoPerm == perm.RepoPermWrite || p.RepoPerm == perm.RepoPermAdmin) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, "Write perm is required to lock the environment.") - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to get the perm.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - // Build the modal with unlocked envs. - config, err := s.i.GetConfig(ctx, cu.Edges.User, r) - if err != nil { - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - locks, err := s.i.ListLocksOfRepo(ctx, r) - if err != nil { - s.log.Error("It has failed to list locks.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - cb, err := s.i.CreateCallback(ctx, &ent.Callback{ - Type: callback.TypeLock, - RepoID: r.ID, - }) - if err != nil { - s.log.Error("It has failed to create a new callback.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - _, err = slack.New(cu.BotToken). - OpenViewContext(ctx, cmd.TriggerID, buildLockView(cb.Hash, config, locks)) - if err != nil { - s.log.Error("It has failed to open a new view.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - c.Status(http.StatusOK) -} - -func buildLockView(callbackID string, c *extent.Config, locks []*ent.Lock) slack.ModalViewRequest { - hasLocked := func(env string) bool { - for _, lock := range locks { - if lock.Env == env { - return true - } - } - - return false - } - - envs := []*slack.OptionBlockObject{} - for _, env := range c.Envs { - if hasLocked(env.Name) { - continue - } - - envs = append(envs, &slack.OptionBlockObject{ - Text: &slack.TextBlockObject{ - Type: slack.PlainTextType, - Text: env.Name, - }, - Value: env.Name, - }) - } - - return slack.ModalViewRequest{ - Type: slack.VTModal, - CallbackID: callbackID, - Title: slack.NewTextBlockObject(slack.PlainTextType, "Lock", false, false), - Submit: slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false), - Close: slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false), - Blocks: slack.Blocks{ - BlockSet: []slack.Block{ - slack.NewInputBlock( - blockEnv, - slack.NewTextBlockObject(slack.PlainTextType, "Environment", false, false), - slack.NewOptionsSelectBlockElement( - slack.OptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select the environment", false, false), - actionEnv, - envs..., - ), - ), - }, - }, - } -} - -func (s *Slack) handleUnlockCmd(c *gin.Context) { - ctx := c.Request.Context() - - av, _ := c.Get(KeyCmd) - cmd := av.(slack.SlashCommand) - - bv, _ := c.Get(KeyChatUser) - cu := bv.(*ent.ChatUser) - - s.log.Debug("Processing lock command.", zap.String("command", cmd.Text)) - ns, n := parseCmd(cmd.Text) - - r, err := s.i.FindRepoOfUserByNamespaceName(ctx, cu.Edges.User, ns, n) - if ent.IsNotFound(err) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, fmt.Sprintf("The `%s/%s` repository is not found.", ns, n)) - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to get the repo.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - // Validate the perm for the repo. - if p, err := s.i.FindPermOfRepo(ctx, r, cu.Edges.User); !(p.RepoPerm == perm.RepoPermWrite || p.RepoPerm == perm.RepoPermAdmin) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, "Write perm is required to lock the environment.") - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to get the perm.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - // Build the modal with unlocked envs. - locks, err := s.i.ListLocksOfRepo(ctx, r) - if len(locks) == 0 { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, fmt.Sprintf("There is no locked environments in the `%s/%s` repository.", ns, n)) - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to list locks.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - cb, err := s.i.CreateCallback(ctx, &ent.Callback{ - Type: callback.TypeUnlock, - RepoID: r.ID, - }) - if err != nil { - s.log.Error("It has failed to create a new callback.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - _, err = slack.New(cu.BotToken). - OpenViewContext(ctx, cmd.TriggerID, buildUnlockView(cb.Hash, locks)) - if err != nil { - s.log.Error("It has failed to open a new view.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - c.Status(http.StatusOK) -} - -func buildUnlockView(callbackID string, locks []*ent.Lock) slack.ModalViewRequest { - envs := []*slack.OptionBlockObject{} - for _, lock := range locks { - var txt string - if lock.Edges.User != nil { - ca, _ := goment.New(lock.CreatedAt) - txt = fmt.Sprintf("%s - Locked by %s %s", lock.Env, lock.Edges.User.Login, ca.FromNow()) - } else { - ca, _ := goment.New(lock.CreatedAt) - txt = fmt.Sprintf("%s - Locked %s", lock.Env, ca.FromNow()) - } - - envs = append(envs, &slack.OptionBlockObject{ - Text: &slack.TextBlockObject{ - Type: slack.PlainTextType, - Text: txt, - }, - Value: lock.Env, - }) - } - - return slack.ModalViewRequest{ - Type: slack.VTModal, - CallbackID: callbackID, - Title: slack.NewTextBlockObject(slack.PlainTextType, "Unlock", false, false), - Submit: slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false), - Close: slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false), - Blocks: slack.Blocks{ - BlockSet: []slack.Block{ - slack.NewInputBlock( - blockEnv, - slack.NewTextBlockObject(slack.PlainTextType, "Environment", false, false), - slack.NewOptionsSelectBlockElement( - slack.OptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select the environment", false, false), - actionEnv, - envs..., - ), - ), - }, - }, - } -} - -func (s *Slack) interactLock(c *gin.Context) { - ctx := c.Request.Context() - - iv, _ := c.Get(KeyIntr) - itr := iv.(slack.InteractionCallback) - - cv, _ := c.Get(KeyChatUser) - cu := cv.(*ent.ChatUser) - - cb, _ := s.i.FindCallbackByHash(ctx, itr.View.CallbackID) - - sm := parseLockViewSubmissions(itr) - - if _, err := s.i.CreateLock(ctx, &ent.Lock{ - Env: sm.Env, - UserID: cu.Edges.User.ID, - RepoID: cb.Edges.Repo.ID, - }); err != nil { - s.log.Error("It has failed to lock the environment.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - postBotMessage(cu, fmt.Sprintf("Success to lock the `%s` environment of the `%s` repository.", sm.Env, cb.Edges.Repo.GetFullName())) - c.Status(http.StatusOK) -} - -func (s *Slack) interactUnlock(c *gin.Context) { - ctx := c.Request.Context() - - iv, _ := c.Get(KeyIntr) - itr := iv.(slack.InteractionCallback) - - cv, _ := c.Get(KeyChatUser) - cu := cv.(*ent.ChatUser) - - cb, _ := s.i.FindCallbackByHash(ctx, itr.View.CallbackID) - - sm := parseLockViewSubmissions(itr) - - lock, err := s.i.FindLockOfRepoByEnv(ctx, cb.Edges.Repo, sm.Env) - if ent.IsNotFound(err) { - postBotMessage(cu, fmt.Sprintf("The `%s` environment is not locked.", sm.Env)) - c.Status(http.StatusOK) - } else if err != nil { - s.log.Error("It has failed to find the lock.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - if err := s.i.DeleteLock(ctx, lock); err != nil { - s.log.Error("It has failed to unlock the environment.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - postBotMessage(cu, fmt.Sprintf("Success to unlock the `%s` environment of the `%s` repository.", sm.Env, cb.Edges.Repo.GetFullName())) - c.Status(http.StatusOK) -} - -func parseLockViewSubmissions(itr slack.InteractionCallback) *lockViewSubmission { - sm := &lockViewSubmission{} - - values := itr.View.State.Values - if v, ok := values[blockEnv][actionEnv]; ok { - sm.Env = v.SelectedOption.Value - } - - return sm -} diff --git a/internal/server/slack/middlewares.go b/internal/server/slack/middlewares.go deleted file mode 100644 index 33694b7e..00000000 --- a/internal/server/slack/middlewares.go +++ /dev/null @@ -1,133 +0,0 @@ -package slack - -import ( - "bytes" - "io/ioutil" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/gitploy-io/gitploy/model/ent" - "github.com/slack-go/slack" - "go.uber.org/zap" -) - -const ( - KeyCmd = "gitploy.slack.command" - KeyIntr = "gitploy.slack.interaction" - KeyChatUser = "gitploy.slack.user" -) - -type ( - SlackMiddleware struct { - i Interactor - secret string - log *zap.Logger - } - - SlackMiddlewareConfig struct { - Interactor Interactor - Secret string - } -) - -func NewSlackMiddleware(c *SlackMiddlewareConfig) *SlackMiddleware { - return &SlackMiddleware{ - i: c.Interactor, - secret: c.Secret, - log: zap.L().Named("slack-middleware"), - } -} - -func (m *SlackMiddleware) Verify() gin.HandlerFunc { - return func(c *gin.Context) { - v, err := slack.NewSecretsVerifier(c.Request.Header, m.secret) - if err != nil { - m.log.Error("failed to generate the verifier.") - c.AbortWithStatus(http.StatusInternalServerError) - } - - d := copyRawData(c.Request) - v.Write(d) - - if err := v.Ensure(); err != nil { - m.log.Error("invalid request.", zap.Error(err)) - c.AbortWithStatus(http.StatusBadRequest) - } - } -} - -func (m *SlackMiddleware) ParseCmd() gin.HandlerFunc { - return func(c *gin.Context) { - cmd, err := slack.SlashCommandParse(c.Request) - if err != nil { - m.log.Error("It has failed to parse the command.", zap.Error(err)) - c.AbortWithStatus(http.StatusInternalServerError) - return - } - - c.Set(KeyCmd, cmd) - } -} - -func (m *SlackMiddleware) ParseIntr() gin.HandlerFunc { - return func(c *gin.Context) { - c.Request.ParseForm() - payload := c.Request.PostForm.Get("payload") - - intr := slack.InteractionCallback{} - if err := intr.UnmarshalJSON([]byte(payload)); err != nil { - m.log.Error("It has failed to parse the interaction callback.", zap.Error(err)) - c.AbortWithStatus(http.StatusInternalServerError) - return - } - - c.Set(KeyIntr, intr) - } -} - -func (m *SlackMiddleware) SetChatUser() gin.HandlerFunc { - return func(c *gin.Context) { - ctx := c.Request.Context() - - if v, ok := c.Get(KeyCmd); ok { - cmd := v.(slack.SlashCommand) - - cu, err := m.i.FindChatUserByID(ctx, cmd.UserID) - if ent.IsNotFound(err) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, "Slack is not connected with Gitploy.") - c.Status(http.StatusOK) - return - } else if err != nil { - m.log.Error("It has failed to get chat-user.", zap.Error(err)) - c.AbortWithStatus(http.StatusInternalServerError) - return - } - - c.Set(KeyChatUser, cu) - return - } - - if v, ok := c.Get(KeyIntr); ok { - intr := v.(slack.InteractionCallback) - - cu, err := m.i.FindChatUserByID(ctx, intr.User.ID) - // InteractionCallback doesn't have the response URL. - if err != nil { - m.log.Error("It has failed to get chat-user.", zap.Error(err)) - c.AbortWithStatus(http.StatusInternalServerError) - return - } - - c.Set(KeyChatUser, cu) - return - } - } -} - -func copyRawData(req *http.Request) []byte { - bodyBytes, _ := ioutil.ReadAll(req.Body) - req.Body.Close() - - req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) - return bodyBytes -} diff --git a/internal/server/slack/mock/interactor.go b/internal/server/slack/mock/interactor.go deleted file mode 100644 index 84fdccb2..00000000 --- a/internal/server/slack/mock/interactor.go +++ /dev/null @@ -1,452 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./interface.go - -// Package mock is a generated GoMock package. -package mock - -import ( - context "context" - reflect "reflect" - - ent "github.com/gitploy-io/gitploy/model/ent" - extent "github.com/gitploy-io/gitploy/model/extent" - gomock "github.com/golang/mock/gomock" -) - -// MockInteractor is a mock of Interactor interface. -type MockInteractor struct { - ctrl *gomock.Controller - recorder *MockInteractorMockRecorder -} - -// MockInteractorMockRecorder is the mock recorder for MockInteractor. -type MockInteractorMockRecorder struct { - mock *MockInteractor -} - -// NewMockInteractor creates a new mock instance. -func NewMockInteractor(ctrl *gomock.Controller) *MockInteractor { - mock := &MockInteractor{ctrl: ctrl} - mock.recorder = &MockInteractorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockInteractor) EXPECT() *MockInteractorMockRecorder { - return m.recorder -} - -// CheckNotificationRecordOfEvent mocks base method. -func (m *MockInteractor) CheckNotificationRecordOfEvent(ctx context.Context, e *ent.Event) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CheckNotificationRecordOfEvent", ctx, e) - ret0, _ := ret[0].(bool) - return ret0 -} - -// CheckNotificationRecordOfEvent indicates an expected call of CheckNotificationRecordOfEvent. -func (mr *MockInteractorMockRecorder) CheckNotificationRecordOfEvent(ctx, e interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckNotificationRecordOfEvent", reflect.TypeOf((*MockInteractor)(nil).CheckNotificationRecordOfEvent), ctx, e) -} - -// CreateCallback mocks base method. -func (m *MockInteractor) CreateCallback(ctx context.Context, cb *ent.Callback) (*ent.Callback, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCallback", ctx, cb) - ret0, _ := ret[0].(*ent.Callback) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateCallback indicates an expected call of CreateCallback. -func (mr *MockInteractorMockRecorder) CreateCallback(ctx, cb interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCallback", reflect.TypeOf((*MockInteractor)(nil).CreateCallback), ctx, cb) -} - -// CreateChatUser mocks base method. -func (m *MockInteractor) CreateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateChatUser", ctx, cu) - ret0, _ := ret[0].(*ent.ChatUser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateChatUser indicates an expected call of CreateChatUser. -func (mr *MockInteractorMockRecorder) CreateChatUser(ctx, cu interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChatUser", reflect.TypeOf((*MockInteractor)(nil).CreateChatUser), ctx, cu) -} - -// CreateEvent mocks base method. -func (m *MockInteractor) CreateEvent(ctx context.Context, e *ent.Event) (*ent.Event, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateEvent", ctx, e) - ret0, _ := ret[0].(*ent.Event) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateEvent indicates an expected call of CreateEvent. -func (mr *MockInteractorMockRecorder) CreateEvent(ctx, e interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEvent", reflect.TypeOf((*MockInteractor)(nil).CreateEvent), ctx, e) -} - -// CreateLock mocks base method. -func (m *MockInteractor) CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateLock", ctx, l) - ret0, _ := ret[0].(*ent.Lock) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateLock indicates an expected call of CreateLock. -func (mr *MockInteractorMockRecorder) CreateLock(ctx, l interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLock", reflect.TypeOf((*MockInteractor)(nil).CreateLock), ctx, l) -} - -// DeleteChatUser mocks base method. -func (m *MockInteractor) DeleteChatUser(ctx context.Context, cu *ent.ChatUser) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteChatUser", ctx, cu) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteChatUser indicates an expected call of DeleteChatUser. -func (mr *MockInteractorMockRecorder) DeleteChatUser(ctx, cu interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatUser", reflect.TypeOf((*MockInteractor)(nil).DeleteChatUser), ctx, cu) -} - -// DeleteLock mocks base method. -func (m *MockInteractor) DeleteLock(ctx context.Context, l *ent.Lock) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteLock", ctx, l) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteLock indicates an expected call of DeleteLock. -func (mr *MockInteractorMockRecorder) DeleteLock(ctx, l interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLock", reflect.TypeOf((*MockInteractor)(nil).DeleteLock), ctx, l) -} - -// Deploy mocks base method. -func (m *MockInteractor) Deploy(ctx context.Context, u *ent.User, re *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Deploy", ctx, u, re, d, env) - ret0, _ := ret[0].(*ent.Deployment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Deploy indicates an expected call of Deploy. -func (mr *MockInteractorMockRecorder) Deploy(ctx, u, re, d, env interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockInteractor)(nil).Deploy), ctx, u, re, d, env) -} - -// FindCallbackByHash mocks base method. -func (m *MockInteractor) FindCallbackByHash(ctx context.Context, hash string) (*ent.Callback, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindCallbackByHash", ctx, hash) - ret0, _ := ret[0].(*ent.Callback) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindCallbackByHash indicates an expected call of FindCallbackByHash. -func (mr *MockInteractorMockRecorder) FindCallbackByHash(ctx, hash interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindCallbackByHash", reflect.TypeOf((*MockInteractor)(nil).FindCallbackByHash), ctx, hash) -} - -// FindChatUserByID mocks base method. -func (m *MockInteractor) FindChatUserByID(ctx context.Context, id string) (*ent.ChatUser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindChatUserByID", ctx, id) - ret0, _ := ret[0].(*ent.ChatUser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindChatUserByID indicates an expected call of FindChatUserByID. -func (mr *MockInteractorMockRecorder) FindChatUserByID(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChatUserByID", reflect.TypeOf((*MockInteractor)(nil).FindChatUserByID), ctx, id) -} - -// FindDeploymentByID mocks base method. -func (m *MockInteractor) FindDeploymentByID(ctx context.Context, id int) (*ent.Deployment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindDeploymentByID", ctx, id) - ret0, _ := ret[0].(*ent.Deployment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindDeploymentByID indicates an expected call of FindDeploymentByID. -func (mr *MockInteractorMockRecorder) FindDeploymentByID(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindDeploymentByID", reflect.TypeOf((*MockInteractor)(nil).FindDeploymentByID), ctx, id) -} - -// FindLockByID mocks base method. -func (m *MockInteractor) FindLockByID(ctx context.Context, id int) (*ent.Lock, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindLockByID", ctx, id) - ret0, _ := ret[0].(*ent.Lock) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindLockByID indicates an expected call of FindLockByID. -func (mr *MockInteractorMockRecorder) FindLockByID(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindLockByID", reflect.TypeOf((*MockInteractor)(nil).FindLockByID), ctx, id) -} - -// FindLockOfRepoByEnv mocks base method. -func (m *MockInteractor) FindLockOfRepoByEnv(ctx context.Context, r *ent.Repo, env string) (*ent.Lock, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindLockOfRepoByEnv", ctx, r, env) - ret0, _ := ret[0].(*ent.Lock) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindLockOfRepoByEnv indicates an expected call of FindLockOfRepoByEnv. -func (mr *MockInteractorMockRecorder) FindLockOfRepoByEnv(ctx, r, env interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindLockOfRepoByEnv", reflect.TypeOf((*MockInteractor)(nil).FindLockOfRepoByEnv), ctx, r, env) -} - -// FindPermOfRepo mocks base method. -func (m *MockInteractor) FindPermOfRepo(ctx context.Context, r *ent.Repo, u *ent.User) (*ent.Perm, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindPermOfRepo", ctx, r, u) - ret0, _ := ret[0].(*ent.Perm) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindPermOfRepo indicates an expected call of FindPermOfRepo. -func (mr *MockInteractorMockRecorder) FindPermOfRepo(ctx, r, u interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPermOfRepo", reflect.TypeOf((*MockInteractor)(nil).FindPermOfRepo), ctx, r, u) -} - -// FindRepoOfUserByNamespaceName mocks base method. -func (m *MockInteractor) FindRepoOfUserByNamespaceName(ctx context.Context, u *ent.User, namespace, name string) (*ent.Repo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindRepoOfUserByNamespaceName", ctx, u, namespace, name) - ret0, _ := ret[0].(*ent.Repo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindRepoOfUserByNamespaceName indicates an expected call of FindRepoOfUserByNamespaceName. -func (mr *MockInteractorMockRecorder) FindRepoOfUserByNamespaceName(ctx, u, namespace, name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRepoOfUserByNamespaceName", reflect.TypeOf((*MockInteractor)(nil).FindRepoOfUserByNamespaceName), ctx, u, namespace, name) -} - -// FindUserByID mocks base method. -func (m *MockInteractor) FindUserByID(ctx context.Context, id int64) (*ent.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindUserByID", ctx, id) - ret0, _ := ret[0].(*ent.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindUserByID indicates an expected call of FindUserByID. -func (mr *MockInteractorMockRecorder) FindUserByID(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUserByID", reflect.TypeOf((*MockInteractor)(nil).FindUserByID), ctx, id) -} - -// GetBranch mocks base method. -func (m *MockInteractor) GetBranch(ctx context.Context, u *ent.User, r *ent.Repo, branch string) (*extent.Branch, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBranch", ctx, u, r, branch) - ret0, _ := ret[0].(*extent.Branch) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBranch indicates an expected call of GetBranch. -func (mr *MockInteractorMockRecorder) GetBranch(ctx, u, r, branch interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBranch", reflect.TypeOf((*MockInteractor)(nil).GetBranch), ctx, u, r, branch) -} - -// GetCommit mocks base method. -func (m *MockInteractor) GetCommit(ctx context.Context, u *ent.User, r *ent.Repo, sha string) (*extent.Commit, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCommit", ctx, u, r, sha) - ret0, _ := ret[0].(*extent.Commit) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCommit indicates an expected call of GetCommit. -func (mr *MockInteractorMockRecorder) GetCommit(ctx, u, r, sha interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommit", reflect.TypeOf((*MockInteractor)(nil).GetCommit), ctx, u, r, sha) -} - -// GetConfig mocks base method. -func (m *MockInteractor) GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*extent.Config, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConfig", ctx, u, r) - ret0, _ := ret[0].(*extent.Config) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetConfig indicates an expected call of GetConfig. -func (mr *MockInteractorMockRecorder) GetConfig(ctx, u, r interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockInteractor)(nil).GetConfig), ctx, u, r) -} - -// GetTag mocks base method. -func (m *MockInteractor) GetTag(ctx context.Context, u *ent.User, r *ent.Repo, tag string) (*extent.Tag, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTag", ctx, u, r, tag) - ret0, _ := ret[0].(*extent.Tag) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTag indicates an expected call of GetTag. -func (mr *MockInteractorMockRecorder) GetTag(ctx, u, r, tag interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTag", reflect.TypeOf((*MockInteractor)(nil).GetTag), ctx, u, r, tag) -} - -// HasLockOfRepoForEnv mocks base method. -func (m *MockInteractor) HasLockOfRepoForEnv(ctx context.Context, r *ent.Repo, env string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasLockOfRepoForEnv", ctx, r, env) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// HasLockOfRepoForEnv indicates an expected call of HasLockOfRepoForEnv. -func (mr *MockInteractorMockRecorder) HasLockOfRepoForEnv(ctx, r, env interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasLockOfRepoForEnv", reflect.TypeOf((*MockInteractor)(nil).HasLockOfRepoForEnv), ctx, r, env) -} - -// ListDeploymentsOfRepo mocks base method. -func (m *MockInteractor) ListDeploymentsOfRepo(ctx context.Context, r *ent.Repo, env, status string, page, perPage int) ([]*ent.Deployment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListDeploymentsOfRepo", ctx, r, env, status, page, perPage) - ret0, _ := ret[0].([]*ent.Deployment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListDeploymentsOfRepo indicates an expected call of ListDeploymentsOfRepo. -func (mr *MockInteractorMockRecorder) ListDeploymentsOfRepo(ctx, r, env, status, page, perPage interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDeploymentsOfRepo", reflect.TypeOf((*MockInteractor)(nil).ListDeploymentsOfRepo), ctx, r, env, status, page, perPage) -} - -// ListLocksOfRepo mocks base method. -func (m *MockInteractor) ListLocksOfRepo(ctx context.Context, r *ent.Repo) ([]*ent.Lock, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListLocksOfRepo", ctx, r) - ret0, _ := ret[0].([]*ent.Lock) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListLocksOfRepo indicates an expected call of ListLocksOfRepo. -func (mr *MockInteractorMockRecorder) ListLocksOfRepo(ctx, r interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLocksOfRepo", reflect.TypeOf((*MockInteractor)(nil).ListLocksOfRepo), ctx, r) -} - -// ListPermsOfRepo mocks base method. -func (m *MockInteractor) ListPermsOfRepo(ctx context.Context, r *ent.Repo, q string, page, perPage int) ([]*ent.Perm, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPermsOfRepo", ctx, r, q, page, perPage) - ret0, _ := ret[0].([]*ent.Perm) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPermsOfRepo indicates an expected call of ListPermsOfRepo. -func (mr *MockInteractorMockRecorder) ListPermsOfRepo(ctx, r, q, page, perPage interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPermsOfRepo", reflect.TypeOf((*MockInteractor)(nil).ListPermsOfRepo), ctx, r, q, page, perPage) -} - -// SubscribeEvent mocks base method. -func (m *MockInteractor) SubscribeEvent(fn func(*ent.Event)) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubscribeEvent", fn) - ret0, _ := ret[0].(error) - return ret0 -} - -// SubscribeEvent indicates an expected call of SubscribeEvent. -func (mr *MockInteractorMockRecorder) SubscribeEvent(fn interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeEvent", reflect.TypeOf((*MockInteractor)(nil).SubscribeEvent), fn) -} - -// UnsubscribeEvent mocks base method. -func (m *MockInteractor) UnsubscribeEvent(fn func(*ent.Event)) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UnsubscribeEvent", fn) - ret0, _ := ret[0].(error) - return ret0 -} - -// UnsubscribeEvent indicates an expected call of UnsubscribeEvent. -func (mr *MockInteractorMockRecorder) UnsubscribeEvent(fn interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsubscribeEvent", reflect.TypeOf((*MockInteractor)(nil).UnsubscribeEvent), fn) -} - -// UpdateChatUser mocks base method. -func (m *MockInteractor) UpdateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChatUser", ctx, cu) - ret0, _ := ret[0].(*ent.ChatUser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateChatUser indicates an expected call of UpdateChatUser. -func (mr *MockInteractorMockRecorder) UpdateChatUser(ctx, cu interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatUser", reflect.TypeOf((*MockInteractor)(nil).UpdateChatUser), ctx, cu) -} - -// UpdateRepo mocks base method. -func (m *MockInteractor) UpdateRepo(ctx context.Context, r *ent.Repo) (*ent.Repo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateRepo", ctx, r) - ret0, _ := ret[0].(*ent.Repo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateRepo indicates an expected call of UpdateRepo. -func (mr *MockInteractorMockRecorder) UpdateRepo(ctx, r interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRepo", reflect.TypeOf((*MockInteractor)(nil).UpdateRepo), ctx, r) -} diff --git a/internal/server/slack/rollback.go b/internal/server/slack/rollback.go deleted file mode 100644 index 7c8c7819..00000000 --- a/internal/server/slack/rollback.go +++ /dev/null @@ -1,278 +0,0 @@ -package slack - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - "github.com/nleeper/goment" - "github.com/slack-go/slack" - "go.uber.org/zap" - - "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/ent/callback" - "github.com/gitploy-io/gitploy/model/ent/deployment" - "github.com/gitploy-io/gitploy/model/ent/event" - "github.com/gitploy-io/gitploy/model/extent" -) - -const ( - blockDeployment = "block_deployment" - actionDeployment = "aciton_deployment" -) - -type ( - rollbackViewSubmission struct { - DeploymentID int - ApproverIDs []string - } - - deploymentAggregation struct { - envName string - deployments []*ent.Deployment - } -) - -// handleRollbackCmd handles rollback command: "/gitploy rollback OWNER/REPO". -func (s *Slack) handleRollbackCmd(c *gin.Context) { - ctx := c.Request.Context() - - av, _ := c.Get(KeyCmd) - cmd := av.(slack.SlashCommand) - - bv, _ := c.Get(KeyChatUser) - cu := bv.(*ent.ChatUser) - - s.log.Debug("Processing rollback command.", zap.String("command", cmd.Text)) - ns, n := parseCmd(cmd.Text) - - r, err := s.i.FindRepoOfUserByNamespaceName(ctx, cu.Edges.User, ns, n) - if ent.IsNotFound(err) { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, fmt.Sprintf("The `%s/%s` repository is not found.", ns, n)) - c.Status(http.StatusOK) - return - } else if err != nil { - s.log.Error("It has failed to get the repo.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - config, err := s.i.GetConfig(ctx, cu.Edges.User, r) - if err != nil { - postResponseWithError(cmd.ChannelID, cmd.ResponseURL, err) - c.Status(http.StatusOK) - return - } - - perms, err := s.i.ListPermsOfRepo(ctx, r, "", 1, 100) - if err != nil { - s.log.Error("It has failed to list permissions.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - perms = s.filterPerms(perms, cu) - - cb, err := s.i.CreateCallback(ctx, &ent.Callback{ - Type: callback.TypeRollback, - RepoID: r.ID, - }) - if err != nil { - s.log.Error("It has failed to create a new callback.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - as := s.getSuccessfulDeploymentAggregation(ctx, r, config) - - _, err = slack.New(cu.BotToken). - OpenViewContext(ctx, cmd.TriggerID, buildRollbackView(cb.Hash, as, perms)) - if err != nil { - s.log.Error("It has failed to open a dialog.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - c.Status(http.StatusOK) -} - -func buildRollbackView(callbackID string, as []*deploymentAggregation, perms []*ent.Perm) slack.ModalViewRequest { - groups := []*slack.OptionGroupBlockObject{} - - for _, a := range as { - options := []*slack.OptionBlockObject{} - - for _, d := range a.deployments { - created, _ := goment.New(d.CreatedAt) - - options = append(options, slack.NewOptionBlockObject( - strconv.Itoa(d.ID), - slack.NewTextBlockObject( - slack.PlainTextType, - fmt.Sprintf("#%d - %s deployed %s", d.ID, d.GetShortRef(), created.FromNow()), - false, false), - nil)) - } - - groups = append(groups, slack.NewOptionGroupBlockElement( - slack.NewTextBlockObject(slack.PlainTextType, string(a.envName), false, false), - options...)) - } - - approvers := []*slack.OptionBlockObject{} - for _, perm := range perms { - u := perm.Edges.User - if u == nil { - continue - } - - approvers = append(approvers, slack.NewOptionBlockObject( - strconv.FormatInt(u.ID, 10), - slack.NewTextBlockObject(slack.PlainTextType, u.Login, false, false), - nil)) - } - - sets := []slack.Block{ - slack.NewInputBlock( - blockDeployment, - slack.NewTextBlockObject(slack.PlainTextType, "Deployments", false, false), - slack.NewOptionsGroupSelectBlockElement( - slack.OptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select target deployment", false, false), - actionDeployment, - groups..., - ), - ), - } - - if len(approvers) > 0 { - sets = append(sets, slack.InputBlock{ - Type: slack.MBTInput, - BlockID: blockApprovers, - Optional: true, - Label: slack.NewTextBlockObject(slack.PlainTextType, "Approvers", false, false), - Element: slack.NewOptionsSelectBlockElement( - slack.MultiOptTypeStatic, - slack.NewTextBlockObject(slack.PlainTextType, "Select approvers", false, false), - actionApprovers, - approvers..., - ), - }) - } - - return slack.ModalViewRequest{ - Type: slack.VTModal, - CallbackID: callbackID, - Title: slack.NewTextBlockObject(slack.PlainTextType, "Rollback", false, false), - Submit: slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false), - Close: slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false), - Blocks: slack.Blocks{ - BlockSet: sets, - }, - } -} - -func (s *Slack) getSuccessfulDeploymentAggregation(ctx context.Context, r *ent.Repo, cf *extent.Config) []*deploymentAggregation { - a := []*deploymentAggregation{} - - for _, env := range cf.Envs { - ds, _ := s.i.ListDeploymentsOfRepo(ctx, r, env.Name, string(deployment.StatusSuccess), 1, 5) - if len(ds) == 0 { - continue - } - - a = append(a, &deploymentAggregation{ - envName: env.Name, - deployments: ds, - }) - } - - return a -} - -func (s *Slack) interactRollback(c *gin.Context) { - ctx := c.Request.Context() - - iv, _ := c.Get(KeyIntr) - itr := iv.(slack.InteractionCallback) - - cv, _ := c.Get(KeyChatUser) - cu := cv.(*ent.ChatUser) - - cb, _ := s.i.FindCallbackByHash(ctx, itr.View.CallbackID) - - sm := parseRollbackSubmissions(itr) - - d, err := s.i.FindDeploymentByID(ctx, sm.DeploymentID) - if err != nil { - s.log.Error("It has failed to find the deployment.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - config, err := s.i.GetConfig(ctx, cu.Edges.User, cb.Edges.Repo) - if err != nil { - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - if err := config.Eval(&extent.EvalValues{}); err != nil { - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - var env *extent.Env - if env = config.GetEnv(d.Env); env == nil { - postBotMessage(cu, "The env is not defined in the config.") - c.Status(http.StatusOK) - return - } - - d, err = s.i.Deploy(ctx, cu.Edges.User, cb.Edges.Repo, &ent.Deployment{ - Type: deployment.Type(d.Type), - Ref: d.Ref, - Sha: d.Sha, - Env: d.Env, - IsRollback: true, - }, env) - if err != nil { - s.log.Error("It has failed to deploy.", zap.Error(err)) - postMessageWithError(cu, err) - c.Status(http.StatusOK) - return - } - - if _, err := s.i.CreateEvent(ctx, &ent.Event{ - Kind: event.KindDeployment, - Type: event.TypeCreated, - DeploymentID: d.ID, - }); err != nil { - s.log.Error("It has failed to create the event.", zap.Error(err)) - } - - c.Status(http.StatusOK) -} - -func parseRollbackSubmissions(itr slack.InteractionCallback) *rollbackViewSubmission { - sm := &rollbackViewSubmission{} - - values := itr.View.State.Values - if v, ok := values[blockDeployment][actionDeployment]; ok { - sm.DeploymentID = atoi(v.SelectedOption.Value) - } - - ids := make([]string, 0) - if v, ok := values[blockApprovers][actionApprovers]; ok { - for _, option := range v.SelectedOptions { - ids = append(ids, option.Value) - } - - sm.ApproverIDs = ids - } - - return sm -} diff --git a/internal/server/slack/rollback_test.go b/internal/server/slack/rollback_test.go deleted file mode 100644 index 064067e1..00000000 --- a/internal/server/slack/rollback_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package slack - -import ( - "context" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/golang/mock/gomock" - "github.com/slack-go/slack" - "go.uber.org/zap" - - "github.com/gitploy-io/gitploy/internal/server/slack/mock" - "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/ent/deployment" - "github.com/gitploy-io/gitploy/model/extent" -) - -func TestSlack_interactRollback(t *testing.T) { - t.Run("Rollback with the returned deployment.", func(t *testing.T) { - m := mock.NewMockInteractor(gomock.NewController(t)) - - // These values are in "./testdata/rollback-interact.json" - const ( - callbackID = "hZUZvJgWhxYvdekUGESXKjSusKWWIRKr" - chatUserID = "U025KUBB2" - deploymentID = 33 - ) - - t.Log("Find the callback which was stored by the Slash command.") - m. - EXPECT(). - FindCallbackByHash(gomock.Any(), callbackID). - Return(&ent.Callback{ - Edges: ent.CallbackEdges{ - Repo: &ent.Repo{ID: 1}, - }, - }, nil) - - t.Log("Find the deployment by ID.") - m. - EXPECT(). - FindDeploymentByID(gomock.Any(), deploymentID). - Return(&ent.Deployment{ - ID: deploymentID, - Type: deployment.TypeCommit, - Ref: "main", - Sha: "ee411aa", - Env: "prod", - }, nil) - - t.Log("Get the config file of the repository.") - m. - EXPECT(). - GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})). - Return(&extent.Config{ - Envs: []*extent.Env{ - {Name: "prod"}, - }, - }, nil) - - t.Log("Roll back with the returned deployment.") - m. - EXPECT(). - Deploy(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{}), &ent.Deployment{ - Type: deployment.TypeCommit, - Ref: "main", - Sha: "ee411aa", - Env: "prod", - IsRollback: true, - }, gomock.AssignableToTypeOf(&extent.Env{})). - DoAndReturn(func(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, e *extent.Env) (*ent.Deployment, error) { - d.ID = deploymentID + 1 - return d, nil - }) - - t.Log("Create a new event") - m. - EXPECT(). - CreateEvent(gomock.Any(), gomock.AssignableToTypeOf(&ent.Event{})). - Return(&ent.Event{}, nil) - - s := &Slack{i: m, log: zap.L()} - - gin.SetMode(gin.ReleaseMode) - router := gin.New() - router.POST("/interact", func(c *gin.Context) { - bytes, _ := ioutil.ReadFile("./testdata/rollback-interact.json") - intr := slack.InteractionCallback{} - intr.UnmarshalJSON(bytes) - c.Set(KeyIntr, intr) - c.Set(KeyChatUser, &ent.ChatUser{ - Edges: ent.ChatUserEdges{ - User: &ent.User{}, - }, - }) - }, s.interactRollback) - - req, _ := http.NewRequest("POST", "/interact", nil) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("w.Code = %d, wanted %d. Body = %v", w.Code, http.StatusOK, w.Body) - } - }) -} diff --git a/internal/server/slack/slack.go b/internal/server/slack/slack.go index 9a71c5c4..13f92d79 100644 --- a/internal/server/slack/slack.go +++ b/internal/server/slack/slack.go @@ -2,33 +2,17 @@ // Use of this source code is governed by the Gitploy Non-Commercial License // that can be found in the LICENSE file. -// +build !oss +//go:build !oss package slack import ( "context" - "net/http" - "regexp" - "github.com/gin-gonic/gin" "github.com/gitploy-io/gitploy/model/ent" - "github.com/gitploy-io/gitploy/model/ent/callback" - "github.com/slack-go/slack" "go.uber.org/zap" ) -const ( - help = "Below are the commands you can use:\n\n" + - "*Deploy*\n" + - "`/gitploy deploy OWNER/REPO` - Create a new deployment for OWNER/REPO.\n\n" + - "*Rollback*\n" + - "`/gitploy rollback OWNER/REPO` - Rollback by the deployment for OWNER/REPO.\n\n" + - "*Lock/Unlock*\n" + - "`/gitploy lock OWNER/REPO` - Lock the environment to disable deploying.\n" + - "`/gitploy unlock OWNER/REPO` - Unlock the environment.\n\n" -) - func NewSlack(c *SlackConfig) *Slack { s := &Slack{ host: c.ServerHost, @@ -44,47 +28,3 @@ func NewSlack(c *SlackConfig) *Slack { return s } - -// Cmd handles Slash command of Slack. -// https://api.slack.com/interactivity/slash-commands -func (s *Slack) Cmd(c *gin.Context) { - av, _ := c.Get(KeyCmd) - cmd := av.(slack.SlashCommand) - - if matched, _ := regexp.MatchString("^deploy[[:blank:]]+[0-9A-Za-z._-]*/[0-9A-Za-z._-]*$", cmd.Text); matched { - s.handleDeployCmd(c) - } else if matched, _ := regexp.MatchString("^rollback[[:blank:]]+[0-9A-Za-z._-]*/[0-9A-Za-z._-]*$", cmd.Text); matched { - s.handleRollbackCmd(c) - } else if matched, _ := regexp.MatchString("^lock[[:blank:]]+[0-9A-Za-z._-]*/[0-9A-Za-z._-]*$", cmd.Text); matched { - s.handleLockCmd(c) - } else if matched, _ := regexp.MatchString("^unlock[[:blank:]]+[0-9A-Za-z._-]*/[0-9A-Za-z._-]*$", cmd.Text); matched { - s.handleUnlockCmd(c) - } else { - postResponseMessage(cmd.ChannelID, cmd.ResponseURL, help) - } -} - -// Interact interacts interactive components (dialog, button). -func (s *Slack) Interact(c *gin.Context) { - ctx := c.Request.Context() - - v, _ := c.Get(KeyIntr) - itr := v.(slack.InteractionCallback) - - cb, err := s.i.FindCallbackByHash(ctx, itr.View.CallbackID) - if err != nil { - s.log.Error("It has failed to find the callback.", zap.Error(err)) - c.Status(http.StatusInternalServerError) - return - } - - if cb.Type == callback.TypeDeploy { - s.interactDeploy(c) - } else if cb.Type == callback.TypeRollback { - s.interactRollback(c) - } else if cb.Type == callback.TypeLock { - s.interactLock(c) - } else if cb.Type == callback.TypeUnlock { - s.interactUnlock(c) - } -} diff --git a/internal/server/slack/slack_oss.go b/internal/server/slack/slack_oss.go index 0b53b000..7d5ecd67 100644 --- a/internal/server/slack/slack_oss.go +++ b/internal/server/slack/slack_oss.go @@ -1,21 +1,7 @@ -// +build oss +//go:build oss package slack -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - func NewSlack(c *SlackConfig) *Slack { return &Slack{} } - -func (s *Slack) Cmd(c *gin.Context) { - c.Status(http.StatusInternalServerError) -} - -func (s *Slack) Interact(c *gin.Context) { - c.Status(http.StatusInternalServerError) -} diff --git a/internal/server/slack/testdata/deploy-interact.json b/internal/server/slack/testdata/deploy-interact.json deleted file mode 100644 index bb2cc380..00000000 --- a/internal/server/slack/testdata/deploy-interact.json +++ /dev/null @@ -1,232 +0,0 @@ -{ - "type": "view_submission", - "team": { - "id": "T024K36ZE3", - "domain": "gitploy" - }, - "user": { - "id": "U025KUBB2", - "username": "octocat", - "name": "octocat", - "team_id": "T024K36ZE3" - }, - "trigger_id": "2343342985366.2150662237479.d8d4101b1404c64b832d43c5768fd2b0", - "view": { - "id": "V02AA2N0H34", - "team_id": "T024K36ZE3", - "type": "modal", - "blocks": [ - { - "type": "input", - "block_id": "block_env", - "label": { - "type": "plain_text", - "text": "Environment", - "emoji": true - }, - "optional": false, - "dispatch_action": false, - "element": { - "type": "static_select", - "action_id": "action_env", - "placeholder": { - "type": "plain_text", - "text": "Select target environment", - "emoji": true - }, - "options": [ - { - "text": { - "type": "plain_text", - "text": "prod", - "emoji": true - }, - "value": "prod" - } - ] - } - }, - { - "type": "input", - "block_id": "block_type", - "label": { - "type": "plain_text", - "text": "Reference Type", - "emoji": true - }, - "optional": false, - "dispatch_action": false, - "element": { - "type": "static_select", - "action_id": "action_type", - "placeholder": { - "type": "plain_text", - "text": "Select your reference type", - "emoji": true - }, - "options": [ - { - "text": { - "type": "plain_text", - "text": "Commit", - "emoji": true - }, - "value": "commit" - }, - { - "text": { - "type": "plain_text", - "text": "Branch", - "emoji": true - }, - "value": "branch" - }, - { - "text": { - "type": "plain_text", - "text": "Tag", - "emoji": true - }, - "value": "tag" - } - ] - } - }, - { - "type": "input", - "block_id": "block_ref", - "label": { - "type": "plain_text", - "text": "Reference", - "emoji": true - }, - "optional": false, - "dispatch_action": false, - "element": { - "type": "plain_text_input", - "action_id": "action_ref", - "placeholder": { - "type": "plain_text", - "text": "E.g. Commit - 25a667d6, Branch - main, Tag - v0.1.2", - "emoji": true - }, - "dispatch_action_config": { - "trigger_actions_on": [ - "on_enter_pressed" - ] - } - } - }, - { - "type": "input", - "block_id": "block_approvers", - "label": { - "type": "plain_text", - "text": "Approvers", - "emoji": true - }, - "optional": true, - "dispatch_action": false, - "element": { - "type": "multi_static_select", - "action_id": "action_approver_ids", - "placeholder": { - "type": "plain_text", - "text": "Select approvers", - "emoji": true - }, - "options": [ - { - "text": { - "type": "plain_text", - "text": "hanjunlee", - "emoji": true - }, - "value": "17633736" - } - ] - } - } - ], - "private_metadata": "", - "callback_id": "nafyVuEqzcchuVmV", - "state": { - "values": { - "block_env": { - "action_env": { - "type": "static_select", - "selected_option": { - "text": { - "type": "plain_text", - "text": "prod", - "emoji": true - }, - "value": "prod" - } - } - }, - "block_type": { - "action_type": { - "type": "static_select", - "selected_option": { - "text": { - "type": "plain_text", - "text": "Branch", - "emoji": true - }, - "value": "branch" - } - } - }, - "block_ref": { - "action_ref": { - "type": "plain_text_input", - "value": "main" - } - }, - "block_approvers": { - "action_approver_ids": { - "type": "multi_static_select", - "selected_options": [ - { - "text": { - "type": "plain_text", - "text": "hanjunlee", - "emoji": true - }, - "value": "17633736" - } - ] - } - } - } - }, - "hash": "1628164073.uijClkp6", - "title": { - "type": "plain_text", - "text": "Deploy", - "emoji": true - }, - "clear_on_close": false, - "notify_on_close": false, - "close": { - "type": "plain_text", - "text": "Close", - "emoji": true - }, - "submit": { - "type": "plain_text", - "text": "Submit", - "emoji": true - }, - "previous_view_id": null, - "root_view_id": "V02AA2N0H34", - "app_id": "24NKFBZT8", - "external_id": "", - "app_installed_team_id": "T024E6ZE3", - "bot_id": "B02YBE1F" - }, - "response_urls": [], - "is_enterprise_install": false, - "enterprise": null -} \ No newline at end of file diff --git a/internal/server/slack/testdata/rollback-interact.json b/internal/server/slack/testdata/rollback-interact.json deleted file mode 100644 index 25aeccf1..00000000 --- a/internal/server/slack/testdata/rollback-interact.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "type": "view_submission", - "team": { - "id": "T024EKG6ZE3", - "domain": "gitploy" - }, - "user": { - "id": "U025KUBB2", - "username": "octocat", - "name": "octocat", - "team_id": "T024EKG6ZE3" - }, - "api_app_id": "A024NKFBZT8", - "token": "KPuw2gHb31oG2GDX5mjbsw6O", - "trigger_id": "2479998291107.2150662237479.a6afd8db96b07e504c2a4dfb22d70e82", - "view": { - "id": "V02E0V16N13", - "team_id": "T024EKG6ZE3", - "type": "modal", - "blocks": [ - { - "type": "input", - "block_id": "block_deployment", - "label": { - "type": "plain_text", - "text": "Deployments", - "emoji": true - }, - "optional": false, - "dispatch_action": false, - "element": { - "type": "static_select", - "action_id": "aciton_deployment", - "placeholder": { - "type": "plain_text", - "text": "Select target deployment", - "emoji": true - }, - "option_groups": [ - { - "label": { - "type": "plain_text", - "text": "local", - "emoji": true - }, - "options": [ - { - "text": { - "type": "plain_text", - "text": "#33 - 36644dd deployed at 15 days ago", - "emoji": true - }, - "value": "33" - }, - { - "text": { - "type": "plain_text", - "text": "#32 - 36644dd deployed at 15 days ago", - "emoji": true - }, - "value": "32" - } - ] - } - ] - } - }, - { - "type": "input", - "block_id": "block_approvers", - "label": { - "type": "plain_text", - "text": "Approvers", - "emoji": true - }, - "optional": true, - "dispatch_action": false, - "element": { - "type": "multi_static_select", - "action_id": "action_approver_ids", - "placeholder": { - "type": "plain_text", - "text": "Select approvers", - "emoji": true - }, - "options": [ - { - "text": { - "type": "plain_text", - "text": "hanjunlee", - "emoji": true - }, - "value": "17633736" - } - ] - } - } - ], - "private_metadata": "", - "callback_id": "hZUZvJgWhxYvdekUGESXKjSusKWWIRKr", - "state": { - "values": { - "block_deployment": { - "aciton_deployment": { - "type": "static_select", - "selected_option": { - "text": { - "type": "plain_text", - "text": "#33 - 36644dd deployed at 15 days ago", - "emoji": true - }, - "value": "33" - } - } - }, - "block_approvers": { - "action_approver_ids": { - "type": "multi_static_select", - "selected_options": [ - { - "text": { - "type": "plain_text", - "text": "hanjunlee", - "emoji": true - }, - "value": "17633736" - } - ] - } - } - } - }, - "hash": "1631327008.N96kHN5Y", - "title": { - "type": "plain_text", - "text": "Rollback", - "emoji": true - }, - "clear_on_close": false, - "notify_on_close": false, - "close": { - "type": "plain_text", - "text": "Close", - "emoji": true - }, - "submit": { - "type": "plain_text", - "text": "Submit", - "emoji": true - }, - "previous_view_id": null, - "root_view_id": "V02E0V16N13", - "app_id": "A024NKFBZT8", - "external_id": "", - "app_installed_team_id": "T024EKG6ZE3", - "bot_id": "B02582YBE1F" - }, - "response_urls": [], - "is_enterprise_install": false, - "enterprise": null -} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 82b118b7..2c37c60c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,7 +18,7 @@ nav: - Review: concepts/review.md - Lock: concepts/lock.md - Permission: concepts/permission.md - - Chatops: concepts/chatops.md + - Notification: concepts/notification.md - Metrics: concepts/metrics.md - License: concepts/license.md - "Self-hosted Server": concepts/self-hosted-server.md