diff --git a/deploy/lock.go b/deploy/lock.go index 1ad9cd2..3b65977 100644 --- a/deploy/lock.go +++ b/deploy/lock.go @@ -3,6 +3,7 @@ package deploy import ( "context" "fmt" + "sort" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -197,8 +198,64 @@ func (c *Coordinator) unlock(ctx context.Context, project, environment, user str return 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) { +type PhaseDesc struct { + Name string + Phase +} + +type ProjectDesc struct { + Name string + Phases []PhaseDesc +} + +func (c *Coordinator) DescribeLocks(ctx context.Context) ([]ProjectDesc, error) { + locks, err := c.describeLocks(ctx) + if err != nil { + return nil, err + } + + priorities := map[string]int{ + "production": 1, + "staging": 2, + } + + var projects []ProjectDesc + for project, phasesMap := range locks { + var phases []PhaseDesc + for name, phase := range phasesMap { + phases = append(phases, PhaseDesc{name, phase}) + } + + sort.SliceStable(phases, func(i, j int) bool { + pi, ok := priorities[phases[i].Name] + if !ok { + pi = 3 + } + + pj, ok := priorities[phases[j].Name] + if !ok { + pj = 3 + } + + if pi != pj { + return pi < pj + } + + return phases[i].Name < phases[j].Name + }) + + projects = append(projects, ProjectDesc{project, phases}) + } + + sort.SliceStable(projects, func(i, j int) bool { + return projects[i].Name < projects[j].Name + }) + + 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) { configMap, err := c.getOrCreateConfigMap(ctx) if err != nil { return nil, err diff --git a/deploy/lock_test.go b/deploy/lock_test.go index 431c91f..f7fa366 100644 --- a/deploy/lock_test.go +++ b/deploy/lock_test.go @@ -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.describeLocks(ctx) require.NoError(t, err) require.Len(t, locks, 1) @@ -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.describeLocks(ctx) require.NoError(t, err) require.Len(t, locks, 1) require.Equal(t, map[string]Phase{ diff --git a/slack.go b/slack.go index 3468c7f..94a9246 100644 --- a/slack.go +++ b/slack.go @@ -307,16 +307,19 @@ func (s *SlackListener) unlock(cmd *slackcmd.Unlock, triggeredBy User, replyIn s // describeLocks describes the locks of all projects and environments, and replies to the given channel. func (s *SlackListener) describeLocks() slack.MsgOption { - locks, err := s.getOrCreateCoordinator().DescribeLocks(context.Background()) + projects, err := s.getOrCreateCoordinator().DescribeLocks(context.Background()) if err != nil { return s.errorMessage(err.Error()) } var buf strings.Builder - for project, envs := range locks { + for _, pj := range projects { + project := pj.Name + envs := pj.Phases buf.WriteString(project) buf.WriteString("\n") - for env, lock := range envs { + for _, lock := range envs { + env := lock.Name buf.WriteString(" ") buf.WriteString(env) buf.WriteString(": ") diff --git a/slack_test.go b/slack_test.go index 07882ae..4374e75 100644 --- a/slack_test.go +++ b/slack_test.go @@ -24,7 +24,7 @@ import ( "k8s.io/client-go/kubernetes" ) -var myprojectConfigMap = corev1.ConfigMap{ +var project1ConfigMap = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "myproject1", Labels: map[string]string{ @@ -33,6 +33,21 @@ var myprojectConfigMap = corev1.ConfigMap{ }, Data: map[string]string{ "Phases": `- name: production +- name: staging +`, + }, +} + +var project2ConfigMap = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myproject2", + Labels: map[string]string{ + "gocat.zaim.net/configmap-type": "project", + }, + }, + Data: map[string]string{ + "Phases": `- name: production +- name: staging `, }, } @@ -70,6 +85,13 @@ user4 }, } +var configMaps = []corev1.ConfigMap{ + project1ConfigMap, + project2ConfigMap, + githubuserMappingConfigMap, + rolebindingConfigMap, +} + // TestSlackLockUnlock tests the lock and unlock commands against // a pre-configured Kubernetes cluster, fake Slack API, and fake GitHub API. // It verifies that the lock and unlock commands work as expected, by sending @@ -83,7 +105,7 @@ func TestSlackLockUnlock(t *testing.T) { clientset, err := k.ClientSet() require.NoError(t, err) - setupConfigMaps(t, clientset, myprojectConfigMap, githubuserMappingConfigMap, rolebindingConfigMap) + setupConfigMaps(t, clientset, configMaps...) setupNamespace(t, clientset, "gocat") messages := make(chan message, 10) @@ -273,6 +295,46 @@ func TestSlackLockUnlock(t *testing.T) { production: Locked (by user2, for deployment of revision a) `, nextMessage().Text()) + // Lock staging + require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ + User: "U1235", + Channel: "C1234", + Text: "lock myproject1 staging for deployment of revision b", + })) + require.Equal(t, "Locked myproject1 staging", nextMessage().Text()) + + // Describe locks + require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ + User: "U1235", + Channel: "C1234", + Text: "describe locks", + })) + require.Equal(t, `myproject1 + production: Locked (by user2, for deployment of revision a) + staging: Locked (by user2, for deployment of revision b) +`, nextMessage().Text()) + + // Lock project 2 staging + require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ + User: "U1235", + Channel: "C1234", + Text: "lock myproject2 staging for deployment of revision c", + })) + require.Equal(t, "Locked myproject2 staging", nextMessage().Text()) + + // Describe locks + require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ + User: "U1235", + Channel: "C1234", + Text: "describe locks", + })) + require.Equal(t, `myproject1 + production: Locked (by user2, for deployment of revision a) + staging: Locked (by user2, for deployment of revision b) +myproject2 + staging: Locked (by user2, for deployment of revision c) +`, nextMessage().Text()) + // User 1 is a developer so cannot unlock the project forcefully require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ User: "U1234", @@ -297,6 +359,30 @@ func TestSlackLockUnlock(t *testing.T) { })) require.Equal(t, `myproject1 production: Unlocked + staging: Locked (by user2, for deployment of revision b) +myproject2 + staging: Locked (by user2, for deployment of revision c) +`, nextMessage().Text()) + + // Unlock project 2 staging + require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ + User: "U1235", + Channel: "C1234", + Text: "unlock myproject2 staging", + })) + require.Equal(t, "Unlocked myproject2 staging", nextMessage().Text()) + + // Describe locks + require.NoError(t, l.handleMessageEvent(&slackevents.AppMentionEvent{ + User: "U1235", + Channel: "C1234", + Text: "describe locks", + })) + require.Equal(t, `myproject1 + production: Unlocked + staging: Locked (by user2, for deployment of revision b) +myproject2 + staging: Unlocked `, nextMessage().Text()) }