Skip to content

Commit

Permalink
Enhance @bot deploy to respect locks
Browse files Browse the repository at this point in the history
  • Loading branch information
mumoshu authored and pirlodog1125 committed Sep 11, 2024
1 parent f74c276 commit 0acf7b3
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 5 deletions.
18 changes: 15 additions & 3 deletions deploy/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ type ProjectDesc struct {
}

func (c *Coordinator) DescribeLocks(ctx context.Context) ([]ProjectDesc, error) {
locks, err := c.describeLocks(ctx)
locks, err := c.FetchLocks(ctx, "", "")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -254,8 +254,11 @@ func (c *Coordinator) DescribeLocks(ctx context.Context) ([]ProjectDesc, error)
return projects, nil
}

// describeLocks returns a map of project names to a map of environment names to the lock information.
func (c *Coordinator) describeLocks(ctx context.Context) (map[string]map[string]Phase, error) {
// FetchLocks returns a map of project names to a map of environment names to the lock information.
//
// Filter can be used to filter the results by `project/phase`.
// If you specify `myproject/production`, only the lock information for the `production` phase of the `myproject` project is returned.
func (c *Coordinator) FetchLocks(ctx context.Context, projectFilter, phaseFilter string) (map[string]map[string]Phase, error) {
configMap, err := c.getOrCreateConfigMap(ctx)
if err != nil {
return nil, err
Expand All @@ -269,6 +272,15 @@ func (c *Coordinator) describeLocks(ctx context.Context) (map[string]map[string]
}

project, environment := splitConfigMapKey(k)

if projectFilter != "" && project != projectFilter {
continue
}

if phaseFilter != "" && environment != phaseFilter {
continue
}

if locks[project] == nil {
locks[project] = make(map[string]Phase)
}
Expand Down
4 changes: 2 additions & 2 deletions deploy/lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestDescribeLocks(t *testing.T) {

require.NoError(t, c.Lock(ctx, "myproject1", "prod", "user1", "for deployment of revision a"))

locks, err := c.describeLocks(ctx)
locks, err := c.FetchLocks(ctx, "", "")
require.NoError(t, err)
require.Len(t, locks, 1)

Expand All @@ -114,7 +114,7 @@ func TestDescribeLocks(t *testing.T) {

require.NoError(t, c.Unlock(ctx, "myproject1", "prod", "user1", false))

locks, err = c.describeLocks(ctx)
locks, err = c.FetchLocks(ctx, "", "")
require.NoError(t, err)
require.Len(t, locks, 1)
require.Equal(t, map[string]Phase{
Expand Down
3 changes: 3 additions & 0 deletions project.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ func (p *ProjectList) Reload() {
pj.filterRegexp = cm.Data["FilterRegexp"]
pj.targetRegexp = cm.Data["TargetRegexp"]
pj.funcName = cm.Data["FuncName"]
// Note that, although this is named Alias, it is actually treated as a
// mandatory ID of the project, which is used to identify the project.
// to be deployed in some places.
pj.Alias = cm.Data["Alias"]
pj.DisableBranchDeploy = cm.Data["DisableBranchDeploy"] == "true"
if err := yaml.Unmarshal([]byte(cm.Data["Steps"]), &pj.steps); err != nil {
Expand Down
39 changes: 39 additions & 0 deletions slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ func (s *SlackListener) handleMessageEvent(ev *slackevents.AppMentionEvent) erro
}

phase := s.toPhase(commands[2])

if msg, locked := s.checkDeploymentLock(target.ID, phase, ev.User, ev.Channel); locked {
if _, _, err := s.client.PostMessage(ev.Channel, msg); err != nil {
log.Println("[ERROR] ", err)
}
return nil
}

interactor := s.interactorFactory.Get(target, phase)
blocks, err := interactor.Request(target, phase, target.DefaultBranch(), ev.User, ev.Channel)
if err != nil {
Expand Down Expand Up @@ -346,6 +354,37 @@ func (s *SlackListener) describeLocks() slack.MsgOption {
return s.infoMessage(buf.String())
}

func (s *SlackListener) checkDeploymentLock(projectID, env string, triggeredBy string, replyIn string) (slack.MsgOption, bool) {
locks, err := s.getOrCreateCoordinator().FetchLocks(context.Background(), projectID, env)
if err != nil {
log.Println("[ERROR] ", err)
if _, _, err := s.client.PostMessage(replyIn, s.infoMessage(err.Error())); err != nil {
log.Println("[ERROR] ", err)
}
return nil, false
}

if len(locks) == 0 {
// Missing lock means it has never been locked.
return nil, false
}

lock, ok := locks[projectID][env]
if !ok {
// Missing lock means it has never been locked.
return nil, false
}

user := s.userList.FindBySlackUserID(triggeredBy)

if lock.Locked && lock.LockHistory[len(lock.LockHistory)-1].User != user.SlackDisplayName {
cause := fmt.Sprintf("locked by %s", lock.LockHistory[len(lock.LockHistory)-1].User)
return s.infoMessage(fmt.Sprintf("Deployment failed: %s", cause)), true
}

return nil, false
}

func (s *SlackListener) validateProjectEnvUser(projectID, env string, user User, replyIn string) error {
pj, err := s.projectList.FindByAlias(projectID)
if err != nil {
Expand Down
34 changes: 34 additions & 0 deletions slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var project1ConfigMap = corev1.ConfigMap{
},
},
Data: map[string]string{
"Alias": "myproject1",
"Phases": `- name: production
- name: staging
`,
Expand All @@ -46,6 +47,7 @@ var project2ConfigMap = corev1.ConfigMap{
},
},
Data: map[string]string{
"Alias": "myproject2",
"Phases": `- name: production
- name: staging
`,
Expand Down Expand Up @@ -380,6 +382,38 @@ myproject2
require.Equal(t, `myproject1
staging: Locked (by user2, for deployment of revision b)
`, nextMessage().Text())

// Deployment to myproject1/staging by user1 should fail because it is locked by user2
require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{
User: "U1234",
Channel: "C1234",
Text: "deploy myproject1 staging",
}))
require.Equal(t, "Deployment failed: locked by user2", nextMessage().Text())

// Deployment to myproject1/staging by user2 should succeed because it is locked by user2
require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{
User: "U1235",
Channel: "C1234",
Text: "deploy myproject1 staging",
}))
require.Equal(t, "**\n*staging*\n*master* ブランチ\nをデプロイしますか?", nextMessage().Text())

// Deployment to myproject1/production by user1 should fail because it is not locked
require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{
User: "U1234",
Channel: "C1234",
Text: "deploy myproject1 production",
}))
require.Equal(t, "**\n*production*\n*master* ブランチ\nをデプロイしますか?", nextMessage().Text())

// Deployment to myproject2/staging by user1 should succeed because it is not locked
require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{
User: "U1234",
Channel: "C1234",
Text: "deploy myproject2 staging",
}))
require.Equal(t, "**\n*staging*\n*master* ブランチ\nをデプロイしますか?", nextMessage().Text())
}

// Message is a message posted to the fake Slack API's chat.postMessage endpoint
Expand Down

0 comments on commit 0acf7b3

Please sign in to comment.