Skip to content
Open
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
74 changes: 54 additions & 20 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package main

import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"text/template"

"gopkg.in/yaml.v2"
)
Expand Down Expand Up @@ -70,12 +72,46 @@ func (cc ConfigChannel) String() string {

// ConfigSlackSync represents a synchronization between a set of PagerDuty schedules and a Slack channel.
type ConfigSlackSync struct {
Name string `yaml:"name"`
Schedules []ConfigSchedule `yaml:"schedules"`
Channel ConfigChannel `yaml:"channel"`
Template string `yaml:"template"`
PretendUsers bool `yaml:"pretendUsers"`
DryRun bool `yaml:"dryRun"`
Name string `yaml:"name"`
Schedules []ConfigSchedule `yaml:"schedules"`
Channel *ConfigChannel `yaml:"channel"`
Template *template.Template `yaml:"-"`
templateString string `yaml:"template"`
PretendUsers bool `yaml:"pretendUsers"`
DryRun bool `yaml:"dryRun"`
}

func (css *ConfigSlackSync) populateChannel(_ context.Context, allChannels channelList) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we could drop the context.Context parameter from the signature here and then again from populateTemplate (unless they are part of some interface that I'm missing)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably, i got in the habit of always including context so you don't have to go back later if things change, but you are right, it doesn't make sense here.

if css.Channel == nil {
return nil
}

slChannel := allChannels.find(css.Channel.ID, css.Channel.Name)
if slChannel == nil {
return fmt.Errorf("failed to find configured Slack channel %s", css.Channel)
}

css.Channel = &ConfigChannel{
ID: slChannel.ID,
Name: slChannel.Name,
}

fmt.Printf("Slack sync %s: found Slack channel %q (ID %s)\n", css.Name, css.Channel.Name, css.Channel.ID)
return nil
}

func (css *ConfigSlackSync) populateTemplate(_ context.Context) error {
if css.templateString == "" {
return nil
}

var err error
css.Template, err = template.New("topic").Parse(css.templateString)
if err != nil {
return fmt.Errorf("failed to parse %s's template %q: %s", css.Name, css.templateString, err)
}

return nil
}

type config struct {
Expand All @@ -95,7 +131,7 @@ func generateConfig(p params) (config, error) {
}
} else {
if p.tmplFile != "" {
b, err := ioutil.ReadFile(p.tmplFile)
b, err := os.ReadFile(p.tmplFile)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good drive-by. 👍

if err != nil {
return config{}, err
}
Expand All @@ -114,22 +150,18 @@ func generateConfig(p params) (config, error) {
cfg.SlackSyncs[i].PretendUsers = *p.pretendUsers
}
}

if p.dryRun != nil {
for i := range cfg.SlackSyncs {
cfg.SlackSyncs[i].DryRun = *p.dryRun
}
}

err = validateConfig(&cfg)
if err != nil {
return config{}, err
}

return cfg, err
}

func readConfigFile(file string) (config, error) {
content, err := ioutil.ReadFile(file)
content, err := os.ReadFile(file)
if err != nil {
return config{}, err
}
Expand All @@ -142,11 +174,13 @@ func readConfigFile(file string) (config, error) {
func singleSlackSync(p params) (config, error) {
slackSync := ConfigSlackSync{
Name: "default",
Channel: ConfigChannel{
}

if len(p.channelID) != 0 || len(p.channelName) != 0 {
slackSync.Channel = &ConfigChannel{
ID: p.channelID,
Name: p.channelName,
},
Template: p.tmplString,
}
}
for _, schedule := range p.schedules {
cfgSchedule, err := parseSchedule(schedule)
Expand Down Expand Up @@ -229,7 +263,7 @@ func parseSchedule(schedule string) (ConfigSchedule, error) {
return cfgSchedule, nil
}

func validateConfig(cfg *config) error {
func (cfg *config) validateConfig() error {
foundNames := map[string]bool{}
for _, sync := range cfg.SlackSyncs {
if _, ok := foundNames[sync.Name]; ok {
Expand All @@ -248,8 +282,8 @@ func validateConfig(cfg *config) error {
}
}

channelGiven := sync.Channel.ID != "" || sync.Channel.Name != ""
if sync.Template != "" {
channelGiven := sync.Channel != nil
if sync.Template != nil {
if !channelGiven {
return fmt.Errorf("slack sync %q invalid: must specify either channel ID or channel name when topic is given", sync.Name)
}
Expand Down
59 changes: 58 additions & 1 deletion config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"
"encoding/json"
"strings"
"testing"

Expand Down Expand Up @@ -53,7 +55,7 @@ func TestParseSchedule(t *testing.T) {
name: "valid schedule with all user group specifiers",
inSchedule: "id=schedule;userGroup=id=123;userGroup=name=user group 2;userGroup=handle=my-ug",
wantCfg: ConfigSchedule{
ID: "schedule",
ID: "schedule",
UserGroups: UserGroups{
{
ID: "123",
Expand Down Expand Up @@ -86,3 +88,58 @@ func TestParseSchedule(t *testing.T) {
})
}
}

func TestPopulateChannel(t *testing.T) {
allChannels := channelList{}
err := json.Unmarshal([]byte(`[{ "id": "1", "name": "Foo" }, { "name": "Bar", "id": "2" }]`), &allChannels)
if err != nil {
t.Fatalf("Unable to convert json to slack: %s", err)
}

tests := []struct {
title string
name string
id string
wantErrStr string
wantChannel *ConfigChannel
}{
{
title: "no match",
name: "foo",
wantErrStr: `failed to find configured Slack channel {ID: Name:"foo"}`,
},
{
title: "By Name",
name: "Foo",
wantChannel: &ConfigChannel{Name: "Foo", ID: "1"},
},
{
title: "By ID",
id: "2",
wantChannel: &ConfigChannel{Name: "Bar", ID: "2"},
},
}

for _, tt := range tests {
t.Run(tt.title, func(t *testing.T) {
cfg := &ConfigSlackSync{
Channel: &ConfigChannel{
ID: tt.id,
Name: tt.name,
},
}
err := cfg.populateChannel(context.Background(), allChannels)
if tt.wantErrStr != "" {
var gotErrStr string
if err != nil {
gotErrStr = err.Error()
}
if !strings.Contains(gotErrStr, tt.wantErrStr) {
t.Errorf("got error string %q, want %q", gotErrStr, tt.wantErrStr)
}
} else if diff := cmp.Diff(tt.wantChannel, cfg.Channel); diff != "" {
t.Errorf("ConfigChannel mismatch (-want +got):\n%s", diff)
}
})
}
}
23 changes: 23 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ By default, the program will terminate after a single run. Use the --daemon flag
&cli.StringFlag{
Name: "config",
Usage: "config file to use",
EnvVars: []string{"CONFIG_FILE"},
Destination: &p.config,
},
&cli.StringSliceFlag{
Expand Down Expand Up @@ -179,11 +180,33 @@ func realMain(p params) error {
}
fmt.Printf("Found %d Slack user group(s)\n", len(sp.slackUserGroups))

fmt.Println("Getting Slack channels")
slChannels, err := sp.slClient.getChannels(ctx)
if err != nil {
return fmt.Errorf("failed to get channels: %s", err)
}
fmt.Printf("Got %d Slack channel(s)\n", len(slChannels))

for _, cfgSlSync := range cfg.SlackSyncs {
if err := cfgSlSync.populateChannel(ctx, slChannels); err != nil {
return fmt.Errorf("failed to populate channel %s", err)
}

if err := cfgSlSync.populateTemplate(ctx); err != nil {
return fmt.Errorf("failed to populate templates: %s", err)
}
}

slSyncs, err := sp.createSlackSyncs(ctx, cfg)
if err != nil {
return fmt.Errorf("failed to create Slack syncs: %s", err)
}

err = cfg.validateConfig()
if err != nil {
return fmt.Errorf("failed to validate config: %s", err)
}

syncer := newSyncer(sp)

runFunc := func() error {
Expand Down
Loading