Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance @bot deploy to respect locks #1143

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading