diff --git a/src/controllers/deploy.go b/src/controllers/deploy.go index 28cbd0a..3af797f 100644 --- a/src/controllers/deploy.go +++ b/src/controllers/deploy.go @@ -9,6 +9,7 @@ import ( "github.com/devclub-iitd/DeployBot/src/helper" "github.com/devclub-iitd/DeployBot/src/history" "github.com/devclub-iitd/DeployBot/src/slack" + "github.com/devclub-iitd/DeployBot/src/discord" log "github.com/sirupsen/logrus" ) @@ -20,6 +21,7 @@ func deploy(callbackID string, data map[string]interface{}) { log.Errorf("cannot post begin deployment chat message - %v", err) return } + go discord.PostActionMessage(callbackID, actionLog.EmbedFields()) log.Infof("beginning %s with callback_id as %s", actionLog, callbackID) logPath := fmt.Sprintf("deploy/%s.txt", callbackID) @@ -38,6 +40,7 @@ func deploy(callbackID string, data map[string]interface{}) { history.StoreAction(actionLog) slack.PostChatMessage(channel, fmt.Sprintf("%s\n", actionLog), nil) } + go discord.PostActionMessage(callbackID, actionLog.EmbedFields()) } // internaldeploy deploys the given app on the server specified. diff --git a/src/controllers/logs.go b/src/controllers/logs.go index 3ae204f..e50fae0 100644 --- a/src/controllers/logs.go +++ b/src/controllers/logs.go @@ -7,6 +7,7 @@ import ( "path" "time" + "github.com/devclub-iitd/DeployBot/src/discord" "github.com/devclub-iitd/DeployBot/src/helper" "github.com/devclub-iitd/DeployBot/src/history" "github.com/devclub-iitd/DeployBot/src/slack" @@ -17,27 +18,33 @@ import ( func logs(callbackID string, data map[string]interface{}) { repoURL := data["git_repo"].(string) channelID := data["channel"].(string) + actionLog := history.NewAction("logs", data) if err := slack.PostChatMessage(channelID, fmt.Sprintf("Fetching logs for %s ...", repoURL), nil); err != nil { log.Warnf("error occured in posting message - %v", err) return } + go discord.PostActionMessage(callbackID, actionLog.EmbedFields()) log.Infof("Fetching logs for service %s with callback_id as %s", repoURL, callbackID) output, err := internalLogs(data) if err != nil { _ = slack.PostChatMessage(channelID, fmt.Sprintf("Logs for service %s could not be fetched.\nERROR: %s", repoURL, err.Error()), nil) + actionLog.Result = "failed" } else { - filePath := path.Join(logDir, "service", fmt.Sprintf("%s.txt", callbackID)) - helper.WriteToFile(filePath, string(output)) - log.Info("starting timer for " + filePath) + actionLog.Result = "success" + filePath := path.Join("service", fmt.Sprintf("%s.txt", callbackID)) + helper.WriteToFile(path.Join(logDir, filePath), string(output)) + actionLog.LogPath = filePath + log.Info("starting timer for log file: %s", filePath) go time.AfterFunc(time.Minute*logsExpiryMins, func() { os.Remove(filePath) - log.Infof("Deleted " + filePath) + log.Infof("Deleted log file: %s", filePath) }) _ = slack.PostChatMessage(channelID, - fmt.Sprintf("Requested logs for service %s would be available at %s/logs/service/%s.txt for %d minutes.", repoURL, serverURL, callbackID, logsExpiryMins), + fmt.Sprintf("Requested logs for service %s would be available at %s/logs/%s for %d minutes.", repoURL, serverURL, filePath, logsExpiryMins), nil) } + go discord.PostActionMessage(callbackID, actionLog.EmbedFields()) } func internalLogs(data map[string]interface{}) ([]byte, error) { diff --git a/src/controllers/stop.go b/src/controllers/stop.go index c2abcff..85078bb 100644 --- a/src/controllers/stop.go +++ b/src/controllers/stop.go @@ -9,6 +9,7 @@ import ( "github.com/devclub-iitd/DeployBot/src/helper" "github.com/devclub-iitd/DeployBot/src/history" "github.com/devclub-iitd/DeployBot/src/slack" + "github.com/devclub-iitd/DeployBot/src/discord" log "github.com/sirupsen/logrus" ) @@ -20,6 +21,7 @@ func stop(callbackID string, data map[string]interface{}) { log.Warnf("error occured in posting chat message - %v", err) return } + go discord.PostActionMessage(callbackID, actionLog.EmbedFields()) log.Infof("beginning %s with callback_id as %s", actionLog, callbackID) logPath := fmt.Sprintf("stop/%s.txt", callbackID) @@ -36,6 +38,7 @@ func stop(callbackID string, data map[string]interface{}) { history.StoreAction(actionLog) slack.PostChatMessage(channel, actionLog.String(), nil) } + go discord.PostActionMessage(callbackID, actionLog.EmbedFields()) } // internalStop actually runs the script to stop the given app. diff --git a/src/discord/config.go b/src/discord/config.go new file mode 100644 index 0000000..f2dadbb --- /dev/null +++ b/src/discord/config.go @@ -0,0 +1,59 @@ +package discord + +import ( + "fmt" + + "github.com/devclub-iitd/DeployBot/src/helper" + log "github.com/sirupsen/logrus" +) + +var ( + // postMessageHookURL is the webhook url for posting messages on discord + postMessageHookURL string + serverURL string +) + +func init() { + postMessageHookURL = helper.Env("DISCORD_MESSAGE_WEBHOOK", "None") + if postMessageHookURL == "None" { + log.Fatal("DISCORD_MESSAGE_WEBHOOK not present in environment, Exiting") + } + serverURL = helper.Env("SERVER_URL", "https://listen.devclub.iitd.ac.in") +} + +type Embed struct { + Title string `json:"title"` + Color int `json:"color"` + Fields []interface{} `json:"fields"` +} + +type Message struct { + Content string `json:"content"` + Embeds []*Embed `json:"embeds"` +} + +func newMessage(callbackID, action, result string, fields []interface{}) Message { + var callbackField interface{} + callbackField = map[string]interface{}{ + "name": "Action ID", + "value": callbackID, + "inline": true, + } + fields = append([]interface{}{callbackField}, fields...) + embed := &Embed{Fields: fields} + switch result { + case "success": + embed.Title = fmt.Sprintf("%s action completed", action) + embed.Color = 6749952 + case "failed": + embed.Title = fmt.Sprintf("%s action failed", action) + embed.Color = 16724736 + default: + embed.Title = fmt.Sprintf("%s action in progress", action) + embed.Color = 3381759 + } + + return Message{ + Embeds: []*Embed{embed}, + } +} diff --git a/src/discord/discord.go b/src/discord/discord.go new file mode 100644 index 0000000..6e88c14 --- /dev/null +++ b/src/discord/discord.go @@ -0,0 +1,28 @@ +package discord + +import ( + "net/http" + "encoding/json" + "fmt" + "bytes" + + log "github.com/sirupsen/logrus" +) + +func PostActionMessage(callbackID string, data map[string]interface{}) error { + msg := newMessage(callbackID, data["action"].(string), data["result"].(string), data["fields"].([]interface{})) + payload, err := json.Marshal(msg) + if err != nil { + log.Errorf("cannot marshal discord message - %v", err) + return err + } + log.Infof("Posting chat message to discord: %+v", msg) + resp, err := http.Post(postMessageHookURL, "application/json", bytes.NewBuffer(payload)) + resp.Body.Close() + if err != nil { + return fmt.Errorf("discord POST request failed - %v", err) + } + log.Infof("Message posted to discord") + return nil +} + diff --git a/src/history/config.go b/src/history/config.go index 1ca8225..75e3d2d 100644 --- a/src/history/config.go +++ b/src/history/config.go @@ -31,6 +31,7 @@ var ( stateFile string // serverURL is the URL of the server serverURL string + domain string statusTemplate *template.Template @@ -116,6 +117,43 @@ func (a *ActionInstance) Fields() []interface{} { return fields } +type EmbedField struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline"` +} + +func (a *ActionInstance) EmbedFields() map[string]interface{} { + m := map[string]interface{}{"action": a.Action, "result": a.Result} + if a.Result != "" { + fields := []interface{}{ + EmbedField{"Result", a.Result, true}, + EmbedField{"Repo", a.RepoURL, false}, + EmbedField{"Logs", fmt.Sprintf("%s/logs/%s", serverURL, a.LogPath), false}, + } + if a.Action == "deploy" && a.Result == "success" { + fields = append(fields, + EmbedField{"URL", fmt.Sprintf("https://%s.%s", a.Subdomain, domain), false}, + ) + } + m["fields"] = fields + return m + } + fields := []interface{}{ + EmbedField{"User", a.User, true}, + EmbedField{"Repo", a.RepoURL, false}, + } + if a.Action == "deploy" { + fields = append(fields, + EmbedField{"Subdomain", a.Subdomain, true}, + EmbedField{"Server", a.Server, true}, + EmbedField{"Access", a.Access, true}, + ) + } + m["fields"] = fields + return m +} + // HealthCheck type stores the result of a healthcheck type HealthCheck struct { Timestamp time.Time `json:"timestamp"` @@ -194,6 +232,7 @@ func newZapLogger(outfile string) (*zap.SugaredLogger, error) { func init() { serverURL = helper.Env("SERVER_URL", "https://listen.devclub.iitd.ac.in") + domain = helper.Env("DOMAIN", "devclub.iitd.ac.in") historyFile = helper.Env("HISTORY_FILE", "/etc/nginx/logs/history.json") healthCheckFile = helper.Env("HEALTH_CHECK_FILE", "/etc/nginx/logs/health.json") stateFile = helper.Env("STATE_FILE", "/etc/nginx/logs/state.json")