Skip to content

Commit

Permalink
Merge pull request #270 from stefafafan/add-channels
Browse files Browse the repository at this point in the history
Implement `mkr channels pull` command
  • Loading branch information
Stefan Utamaru committed Mar 18, 2020
2 parents 7f2927a + 83f7d32 commit fd6f1d3
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 0 deletions.
66 changes: 66 additions & 0 deletions channels/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package channels

import (
"fmt"
"io"
"log"
"os"

"github.com/mackerelio/mackerel-client-go"
"github.com/mackerelio/mkr/format"
"github.com/mackerelio/mkr/logger"
"github.com/mackerelio/mkr/mackerelclient"
)

type channelsApp struct {
client mackerelclient.Client
outStream io.Writer
}

func (app *channelsApp) run() error {
channels, err := app.client.FindChannels()
if err != nil {
return err
}

format.PrettyPrintJSON(app.outStream, channels)
return nil
}

func (app *channelsApp) pullChannels(isVerbose bool, optFilePath string) error {
channels, err := app.client.FindChannels()
logger.DieIf(err)

filePath := "channels.json"
if optFilePath != "" {
filePath = optFilePath
}

saveChannels(channels, filePath)

if isVerbose {
format.PrettyPrintJSON(os.Stdout, channels)
}

logger.Log("info", fmt.Sprintf("Channels are saved to '%s' (%d rules).", filePath, len(channels)))
return nil
}

var saveChannels = channelSaveRules

func channelSaveRules(rules []*mackerel.Channel, filePath string) error {
file, err := os.Create(filePath)
if err != nil {
log.Fatal(err)
}
defer file.Close()

channels := map[string]interface{}{"channels": rules}
data := format.JSONMarshalIndent(channels, "", " ") + "\n"

_, err = file.WriteString(data)
if err != nil {
return err
}
return nil
}
177 changes: 177 additions & 0 deletions channels/app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package channels

import (
"bytes"
"fmt"
"testing"

"github.com/mackerelio/mackerel-client-go"
"github.com/mackerelio/mkr/mackerelclient"
"github.com/stretchr/testify/assert"
)

// boolPointer is a helper function to initialize a bool pointer
func boolPointer(b bool) *bool {
return &b
}

func TestChannelsApp_Run(t *testing.T) {
testCases := []struct {
id string
channels []*mackerel.Channel
expected string
}{
{
id: "default",
channels: []*mackerel.Channel{
&mackerel.Channel{
ID: "abcdefabc",
Name: "email channel",
Type: "email",
Emails: &[]string{"test@example.com", "test2@example.com"},
UserIDs: &[]string{"1234", "2345"},
Events: &[]string{"alert"},
},
&mackerel.Channel{
ID: "bcdefabcd",
Name: "slack channel",
Type: "slack",
URL: "https://hooks.slack.com/services/TAAAA/BBBB/XXXXX",
Mentions: mackerel.Mentions{
OK: "ok message",
Warning: "warning message",
},
EnabledGraphImage: boolPointer(true),
Events: &[]string{"alert"},
},
&mackerel.Channel{
ID: "cdefabcde",
Name: "webhook channel",
Type: "webhook",
URL: "http://example.com/webhook",
Events: &[]string{"alertGroup"},
},
&mackerel.Channel{
ID: "defabcdef",
Name: "line channel",
Type: "line",
},
},
expected: `[
{
"id": "abcdefabc",
"name": "email channel",
"type": "email",
"emails": [
"test@example.com",
"test2@example.com"
],
"userIds": [
"1234",
"2345"
],
"mentions": {},
"events": [
"alert"
]
},
{
"id": "bcdefabcd",
"name": "slack channel",
"type": "slack",
"mentions": {
"ok": "ok message",
"warning": "warning message"
},
"enabledGraphImage": true,
"url": "https://hooks.slack.com/services/TAAAA/BBBB/XXXXX",
"events": [
"alert"
]
},
{
"id": "cdefabcde",
"name": "webhook channel",
"type": "webhook",
"mentions": {},
"url": "http://example.com/webhook",
"events": [
"alertGroup"
]
},
{
"id": "defabcdef",
"name": "line channel",
"type": "line",
"mentions": {}
}
]
`,
},
{
id: "no channels",
channels: []*mackerel.Channel{},
expected: `[]
`,
},
}
for _, tc := range testCases {
client := mackerelclient.NewMockClient(
mackerelclient.MockFindChannels(func() ([]*mackerel.Channel, error) {
return tc.channels, nil
}),
)
t.Run(tc.id, func(t *testing.T) {
out := new(bytes.Buffer)
app := &channelsApp{
client: client,
outStream: out,
}
assert.NoError(t, app.run())
assert.Equal(t, tc.expected, out.String())
})
}
}

func TestChannelsApp_PullChannels(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{
input: "",
expected: "channels.json",
},
{
input: "some_file.json",
expected: "some_file.json",
},
{
input: "hoge.txt",
expected: "hoge.txt",
},
}

for _, tc := range testCases {
out := new(bytes.Buffer)

// override saveChannels to simply print filePath
saveChannels = func(rules []*mackerel.Channel, filePath string) error {
fmt.Fprint(out, filePath)
return nil
}

client := mackerelclient.NewMockClient(
mackerelclient.MockFindChannels(func() ([]*mackerel.Channel, error) {
return []*mackerel.Channel{}, nil
}),
)
app := &channelsApp{
client: client,
outStream: out,
}

assert.NoError(t, app.pullChannels(false, tc.input))
assert.Equal(t, tc.expected, out.String())
}
}
61 changes: 61 additions & 0 deletions channels/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package channels

import (
"os"

"github.com/mackerelio/mkr/mackerelclient"
"github.com/urfave/cli"
)

// Command is the definition of channels subcommand
var Command = cli.Command{
Name: "channels",
Usage: "List notification channels",
Description: `
Lists notification channels. With no subcommand specified, this will show all channels.
Requests APIs under "/api/v0/channels". See https://mackerel.io/api-docs/entry/channels .
`,
Action: doChannels,
Subcommands: []cli.Command{
{
Name: "pull",
Usage: "pull channel settings",
ArgsUsage: "[--file-path | -F <file>] [--verbose | -v]",
Description: `
Pull channels settings from Mackerel server and save them to a file. The file can be specified by filepath argument <file>. The default is 'channels.json'.
`,
Action: doChannelsPull,
Flags: []cli.Flag{
cli.StringFlag{Name: "file-path, F", Value: "", Usage: "Filename to store channel settings. default: channels.json"},
cli.BoolFlag{Name: "verbose, v", Usage: "Verbose output mode"},
},
},
},
}

func doChannels(c *cli.Context) error {
client, err := mackerelclient.New(c.GlobalString("conf"), c.GlobalString("apibase"))
if err != nil {
return err
}

return (&channelsApp{
client: client,
outStream: os.Stdout,
}).run()
}

func doChannelsPull(c *cli.Context) error {
client, err := mackerelclient.New(c.GlobalString("conf"), c.GlobalString("apibase"))
if err != nil {
return err
}

isVerbose := c.Bool("verbose")
filePath := c.String("file-path")

return (&channelsApp{
client: client,
outStream: os.Stdout,
}).pullChannels(isVerbose, filePath)
}
2 changes: 2 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/Songmu/prompter"
"github.com/mackerelio/mackerel-client-go"
"github.com/mackerelio/mkr/channels"
"github.com/mackerelio/mkr/checks"
"github.com/mackerelio/mkr/format"
"github.com/mackerelio/mkr/hosts"
Expand All @@ -31,6 +32,7 @@ var Commands = []cli.Command{
commandRetire,
services.Command,
commandMonitors,
channels.Command,
commandAlerts,
commandDashboards,
commandAnnotations,
Expand Down
1 change: 1 addition & 0 deletions mackerelclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "github.com/mackerelio/mackerel-client-go"
type Client interface {
FindHosts(param *mackerel.FindHostsParam) ([]*mackerel.Host, error)
FindServices() ([]*mackerel.Service, error)
FindChannels() ([]*mackerel.Channel, error)
GetOrg() (*mackerel.Org, error)
CreateHost(param *mackerel.CreateHostParam) (string, error)
UpdateHostStatus(hostID string, status string) error
Expand Down
16 changes: 16 additions & 0 deletions mackerelclient/mock_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "github.com/mackerelio/mackerel-client-go"
type MockClient struct {
findHostsCallback func(param *mackerel.FindHostsParam) ([]*mackerel.Host, error)
findServicesCallback func() ([]*mackerel.Service, error)
findChannelsCallback func() ([]*mackerel.Channel, error)
getOrgCallback func() (*mackerel.Org, error)
createHostCallback func(param *mackerel.CreateHostParam) (string, error)
updateHostStatusCallback func(hostID string, status string) error
Expand Down Expand Up @@ -64,6 +65,21 @@ func MockFindServices(callback func() ([]*mackerel.Service, error)) MockClientOp
}
}

// FindChannels ...
func (c *MockClient) FindChannels() ([]*mackerel.Channel, error) {
if c.findChannelsCallback != nil {
return c.findChannelsCallback()
}
return nil, errCallbackNotFound("FindChannels")
}

// MockFindChannels returns an option to set the callback of FindChannels
func MockFindChannels(callback func() ([]*mackerel.Channel, error)) MockClientOption {
return func(c *MockClient) {
c.findChannelsCallback = callback
}
}

// GetOrg ...
func (c *MockClient) GetOrg() (*mackerel.Org, error) {
if c.getOrgCallback != nil {
Expand Down

0 comments on commit fd6f1d3

Please sign in to comment.