diff --git a/Makefile b/Makefile index e2fb13b3..9a425803 100644 --- a/Makefile +++ b/Makefile @@ -207,13 +207,13 @@ docker-deps: ## mocks: generate Go mocks .PHONY: mocks mocks: - counterfeiter -o ./client/runner/mocks/session.go \ + go run github.com/maxbrunsfeld/counterfeiter/v6 -o ./client/runner/mocks/session.go \ ./client/runner/ssh.go SSHSession - counterfeiter -o ./daemon/inertiad/project/mocks/deployer.go \ + go run github.com/maxbrunsfeld/counterfeiter/v6 -o ./daemon/inertiad/project/mocks/deployer.go \ ./daemon/inertiad/project/deployment.go Deployer - counterfeiter -o ./daemon/inertiad/build/mocks/builder.go \ + go run github.com/maxbrunsfeld/counterfeiter/v6 -o ./daemon/inertiad/build/mocks/builder.go \ ./daemon/inertiad/build/builder.go ContainerBuilder - counterfeiter -o ./daemon/inertiad/notify/mocks/notify.go \ + go run github.com/maxbrunsfeld/counterfeiter/v6 -o ./daemon/inertiad/notify/mocks/notify.go \ ./daemon/inertiad/notify/notifier.go Notifier ## scripts: recompile script assets diff --git a/api/api.go b/api/api.go index 5c3519c1..93e595f0 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,7 @@ type UpRequest struct { GitOptions GitOptions `json:"git_options"` WebHookSecret string `json:"webhook_secret"` IntermediaryContainers []string `json:"intermediary_containers"` + SlackNotificationURL string `json:"slack_notification_url"` } // GitOptions represents GitHub-related deployment options diff --git a/cfg/project.go b/cfg/project.go index 82f668a4..4c8fe254 100644 --- a/cfg/project.go +++ b/cfg/project.go @@ -18,9 +18,10 @@ type Project struct { // Profile denotes a deployment configuration type Profile struct { - Name string `toml:"name"` - Branch string `toml:"branch"` - Build *Build `toml:"build"` + Name string `toml:"name"` + Branch string `toml:"branch"` + Build *Build `toml:"build"` + Notifiers *Notifiers `toml:"notifiers"` } // Identifier implements identity.Identifier @@ -104,3 +105,8 @@ func (p *Project) RemoveProfile(name string) bool { p.Profiles = asProfiles(ids) return ok } + +// Notifiers defines options for notifications on a profile +type Notifiers struct { + SlackNotificationURL string `toml:"slack_notification_url"` +} diff --git a/client/client.go b/client/client.go index 0bf7e222..c66def5d 100644 --- a/client/client.go +++ b/client/client.go @@ -92,6 +92,11 @@ type UpRequest struct { // Up brings the project up on the remote VPS instance specified // in the deployment object. func (c *Client) Up(ctx context.Context, req UpRequest) error { + notif := req.Profile.Notifiers + if notif == nil { + notif = &cfg.Notifiers{} + } + resp, err := c.post(ctx, "/up", &api.UpRequest{ Stream: false, Project: req.Project, @@ -103,6 +108,7 @@ func (c *Client) Up(ctx context.Context, req UpRequest) error { Branch: req.Profile.Branch, }, IntermediaryContainers: req.Profile.Build.IntermediaryContainers, + SlackNotificationURL: notif.SlackNotificationURL, }) if err != nil { return fmt.Errorf("failed to make request: %s", err.Error()) diff --git a/daemon/inertiad/daemon/up.go b/daemon/inertiad/daemon/up.go index ae7eb8ad..2ad50b44 100644 --- a/daemon/inertiad/daemon/up.go +++ b/daemon/inertiad/daemon/up.go @@ -33,14 +33,17 @@ func (s *Server) upHandler(w http.ResponseWriter, r *http.Request) { if upReq.WebHookSecret != "" { s.state.WebhookSecret = upReq.WebHookSecret } - s.deployment.SetConfig(project.DeploymentConfig{ + conf := project.DeploymentConfig{ ProjectName: upReq.Project, BuildType: upReq.BuildType, BuildFilePath: upReq.BuildFilePath, RemoteURL: gitOpts.RemoteURL, Branch: gitOpts.Branch, + PemFilePath: crypto.DaemonGithubKeyLocation, IntermediaryContainers: upReq.IntermediaryContainers, - }) + SlackNotificationURL: upReq.SlackNotificationURL, + } + s.deployment.SetConfig(conf) // Configure streamer var stream = log.NewStreamer(log.StreamerOptions{ @@ -55,17 +58,7 @@ func (s *Server) upHandler(w http.ResponseWriter, r *http.Request) { var skipUpdate = false if status, _ := s.deployment.GetStatus(s.docker); status.CommitHash == "" { stream.Println("No deployment detected") - if err = s.deployment.Initialize( - project.DeploymentConfig{ - ProjectName: upReq.Project, - BuildType: upReq.BuildType, - BuildFilePath: upReq.BuildFilePath, - RemoteURL: gitOpts.RemoteURL, - Branch: gitOpts.Branch, - PemFilePath: crypto.DaemonGithubKeyLocation, - }, - stream, - ); err != nil { + if err = s.deployment.Initialize(conf, stream); err != nil { stream.Error(res.Err(err.Error(), http.StatusPreconditionFailed)) return } diff --git a/daemon/inertiad/notify/mocks/notify.go b/daemon/inertiad/notify/mocks/notify.go index cb6dde23..b377c075 100644 --- a/daemon/inertiad/notify/mocks/notify.go +++ b/daemon/inertiad/notify/mocks/notify.go @@ -8,6 +8,17 @@ import ( ) type FakeNotifier struct { + IsEqualStub func(notify.Notifier) bool + isEqualMutex sync.RWMutex + isEqualArgsForCall []struct { + arg1 notify.Notifier + } + isEqualReturns struct { + result1 bool + } + isEqualReturnsOnCall map[int]struct { + result1 bool + } NotifyStub func(string, notify.Options) error notifyMutex sync.RWMutex notifyArgsForCall []struct { @@ -24,6 +35,66 @@ type FakeNotifier struct { invocationsMutex sync.RWMutex } +func (fake *FakeNotifier) IsEqual(arg1 notify.Notifier) bool { + fake.isEqualMutex.Lock() + ret, specificReturn := fake.isEqualReturnsOnCall[len(fake.isEqualArgsForCall)] + fake.isEqualArgsForCall = append(fake.isEqualArgsForCall, struct { + arg1 notify.Notifier + }{arg1}) + fake.recordInvocation("IsEqual", []interface{}{arg1}) + fake.isEqualMutex.Unlock() + if fake.IsEqualStub != nil { + return fake.IsEqualStub(arg1) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.isEqualReturns + return fakeReturns.result1 +} + +func (fake *FakeNotifier) IsEqualCallCount() int { + fake.isEqualMutex.RLock() + defer fake.isEqualMutex.RUnlock() + return len(fake.isEqualArgsForCall) +} + +func (fake *FakeNotifier) IsEqualCalls(stub func(notify.Notifier) bool) { + fake.isEqualMutex.Lock() + defer fake.isEqualMutex.Unlock() + fake.IsEqualStub = stub +} + +func (fake *FakeNotifier) IsEqualArgsForCall(i int) notify.Notifier { + fake.isEqualMutex.RLock() + defer fake.isEqualMutex.RUnlock() + argsForCall := fake.isEqualArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNotifier) IsEqualReturns(result1 bool) { + fake.isEqualMutex.Lock() + defer fake.isEqualMutex.Unlock() + fake.IsEqualStub = nil + fake.isEqualReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeNotifier) IsEqualReturnsOnCall(i int, result1 bool) { + fake.isEqualMutex.Lock() + defer fake.isEqualMutex.Unlock() + fake.IsEqualStub = nil + if fake.isEqualReturnsOnCall == nil { + fake.isEqualReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.isEqualReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + func (fake *FakeNotifier) Notify(arg1 string, arg2 notify.Options) error { fake.notifyMutex.Lock() ret, specificReturn := fake.notifyReturnsOnCall[len(fake.notifyArgsForCall)] @@ -88,6 +159,8 @@ func (fake *FakeNotifier) NotifyReturnsOnCall(i int, result1 error) { func (fake *FakeNotifier) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.isEqualMutex.RLock() + defer fake.isEqualMutex.RUnlock() fake.notifyMutex.RLock() defer fake.notifyMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/daemon/inertiad/notify/notifier.go b/daemon/inertiad/notify/notifier.go index bed8bc33..b40f1c80 100644 --- a/daemon/inertiad/notify/notifier.go +++ b/daemon/inertiad/notify/notifier.go @@ -20,9 +20,20 @@ func (n Notifiers) Notify(msg string, opts Options) error { return errs } +// Exists checks if the given notifier is already configured +func (n Notifiers) Exists(nt Notifier) bool { + for _, notif := range n { + if notif.IsEqual(nt) { + return true + } + } + return false +} + // Notifier manages notifications type Notifier interface { Notify(string, Options) error + IsEqual(Notifier) bool } // Options is used to configure formatting of notifications diff --git a/daemon/inertiad/notify/notifier_test.go b/daemon/inertiad/notify/notifier_test.go new file mode 100644 index 00000000..79f96a70 --- /dev/null +++ b/daemon/inertiad/notify/notifier_test.go @@ -0,0 +1,26 @@ +package notify + +import "testing" + +func TestNotifiers_Exists(t *testing.T) { + type args struct { + nt Notifier + } + tests := []struct { + name string + n Notifiers + args args + want bool + }{ + {"ok: exists", Notifiers{&SlackNotifier{"abcde"}}, args{&SlackNotifier{"abcde"}}, true}, + {"not ok: no notifiers", Notifiers{}, args{&SlackNotifier{"abcde"}}, false}, + {"not ok: doesnt exist", Notifiers{&SlackNotifier{"robert"}}, args{&SlackNotifier{"abcde"}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.n.Exists(tt.args.nt); got != tt.want { + t.Errorf("Notifiers.Exists() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/daemon/inertiad/notify/slack.go b/daemon/inertiad/notify/slack.go index 687b1bd7..01bea24d 100644 --- a/daemon/inertiad/notify/slack.go +++ b/daemon/inertiad/notify/slack.go @@ -57,6 +57,17 @@ func (n *SlackNotifier) Notify(text string, options Options) error { return nil } +// IsEqual implements Notifier by checking the provided notifier is a slack notifier +// and if it has the same hook URL +func (n *SlackNotifier) IsEqual(nt Notifier) bool { + switch v := nt.(type) { + case *SlackNotifier: + return n.hookURL == v.hookURL + default: + return false + } +} + func colorToString(color Color) string { return string(color) } diff --git a/daemon/inertiad/notify/slack_test.go b/daemon/inertiad/notify/slack_test.go new file mode 100644 index 00000000..45ef8fe4 --- /dev/null +++ b/daemon/inertiad/notify/slack_test.go @@ -0,0 +1,32 @@ +package notify + +import "testing" + +func TestSlackNotifier_IsEqual(t *testing.T) { + type fields struct { + hookURL string + } + type args struct { + nt Notifier + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + {"ok: same hook url", fields{"abcde"}, args{&SlackNotifier{"abcde"}}, true}, + {"not ok: diff hook url", fields{"robert"}, args{&SlackNotifier{"abcde"}}, false}, + {"not ok: not slack notifier", fields{"robert"}, args{nil}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := &SlackNotifier{ + hookURL: tt.fields.hookURL, + } + if got := n.IsEqual(tt.args.nt); got != tt.want { + t.Errorf("SlackNotifier.IsEqual() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/daemon/inertiad/project/deployment.go b/daemon/inertiad/project/deployment.go index c8b27899..9ee5370b 100644 --- a/daemon/inertiad/project/deployment.go +++ b/daemon/inertiad/project/deployment.go @@ -77,6 +77,9 @@ type DeploymentConfig struct { Branch string PemFilePath string IntermediaryContainers []string + + // TODO: maybe improve format for generic notifiers + SlackNotificationURL string } // DeploymentMetadata is used to store metadata relevant @@ -166,7 +169,16 @@ func (d *Deployment) SetConfig(cfg DeploymentConfig) { } d.intermediaryContainers = cfg.IntermediaryContainers - // TODO: register notifiers + // register notifiers + if len(d.notifiers) == 0 { + d.notifiers = notify.Notifiers{} + } + if cfg.SlackNotificationURL != "" { + nt := notify.NewSlackNotifier(cfg.SlackNotificationURL) + if !d.notifiers.Exists(nt) { + d.notifiers = append(d.notifiers, nt) + } + } } // DeployOptions is used to configure how the deployment handles the deploy @@ -215,7 +227,7 @@ func (d *Deployment) Deploy( // Build project deploy, err := d.builder.Build(strings.ToLower(d.buildType), *conf, cli, out) if err != nil { - if notifyErr := d.notifiers.Notify("Build error", notify.Options{ + if notifyErr := d.notifiers.Notify(fmt.Sprintf("Build error: %s", err), notify.Options{ Color: notify.Red, }); notifyErr != nil { fmt.Fprintln(out, notifyErr.Error()) diff --git a/daemon/inertiad/project/deployment_test.go b/daemon/inertiad/project/deployment_test.go index adb8ce12..19659cd8 100644 --- a/daemon/inertiad/project/deployment_test.go +++ b/daemon/inertiad/project/deployment_test.go @@ -25,16 +25,18 @@ func newDefaultFakeBuilder(builder func() error, stopper func() error) *mocks.Fa func TestSetConfig(t *testing.T) { deployment := &Deployment{} deployment.SetConfig(DeploymentConfig{ - ProjectName: "wow", - Branch: "amazing", - BuildType: "best", - BuildFilePath: "/robertcompose.yml", + ProjectName: "wow", + Branch: "amazing", + BuildType: "best", + BuildFilePath: "/robertcompose.yml", + SlackNotificationURL: "https://my.slack.url", }) assert.Equal(t, "wow", deployment.project) assert.Equal(t, "amazing", deployment.branch) assert.Equal(t, "best", deployment.buildType) assert.Equal(t, "/robertcompose.yml", deployment.buildFilePath) + assert.Len(t, deployment.notifiers, 1) } func TestDeployMock(t *testing.T) {