diff --git a/.gitignore b/.gitignore index 7c1659e..bb9413d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config.yml private/ data.sqlite -OBIII \ No newline at end of file +OBIII +OBIII.env \ No newline at end of file diff --git a/OBIII.service b/OBIII.service index ccb7ad9..28522b1 100644 --- a/OBIII.service +++ b/OBIII.service @@ -4,6 +4,7 @@ After=multi-user.target [Service] Type=simple +EnvironmentFile=/root/ops-bot-iii/OBIII.env WorkingDirectory=/root/ops-bot-iii/ Restart=always RestartSec=30 diff --git a/commands/enabled.go b/commands/enabled.go index 8a2c254..0d865cf 100644 --- a/commands/enabled.go +++ b/commands/enabled.go @@ -28,6 +28,7 @@ func populateSlashCommands(ctx ddtrace.SpanContext) { SlashCommands["signin"] = slash.Signin() SlashCommands["vote"] = slash.Vote() SlashCommands["feedback"] = slash.Feedback() + SlashCommands["update"] = slash.Update() } // populateHandlers populates the Handlers map with all of the handlers @@ -63,4 +64,5 @@ func populateScheduledEvents(ctx ddtrace.SpanContext) { ScheduledEvents["goodfood"] = scheduled.GoodFood() ScheduledEvents["heartbeat"] = scheduled.Heartbeat() ScheduledEvents["status"] = scheduled.Status() + ScheduledEvents["update"] = scheduled.Update() } diff --git a/commands/scheduled/update.go b/commands/scheduled/update.go new file mode 100644 index 0000000..e02ea30 --- /dev/null +++ b/commands/scheduled/update.go @@ -0,0 +1,83 @@ +package scheduled + +import ( + "time" + + "github.com/bwmarrin/discordgo" + "github.com/robfig/cron" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/helpers" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/logging" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/structs" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// updates repo, build binary, and exists if update is available +func updateOBIII(s *discordgo.Session, ctx ddtrace.SpanContext) { + span := tracer.StartSpan( + "commands.scheduled.update:updateOBIII", + tracer.ResourceName("Scheduled.Update:updateOBIII"), + tracer.ChildOf(ctx), + ) + defer span.Finish() + + logging.Debug(s, "Checking for update", nil, span) + + update, err := helpers.UpdateMainBranch() + if err != nil { + logging.Error(s, err.Error(), nil, span) + return + } + + if update { + logging.Critical(s, "Update available; updating", nil, span) + + err = helpers.BuildOBIII() + if err != nil { + logging.Error(s, err.Error(), nil, span) + return + } + + err = helpers.Exit() + if err != nil { + logging.Error(s, err.Error(), nil, span) + return + } + } else { + logging.Debug(s, "No update available", nil, span) + } +} + +// checks for update every day at 2am and runs if available +func Update() *structs.ScheduledEvent { + return structs.NewScheduledTask( + func(s *discordgo.Session, quit chan interface{}) error { + span := tracer.StartSpan( + "commands.scheduled.update:Update", + tracer.ResourceName("Scheduled.Update"), + ) + defer span.Finish() + + est, err := time.LoadLocation("America/New_York") + if err != nil { + logging.Error(s, err.Error(), nil, span) + return err + } + + c := cron.NewWithLocation(est) + + // every day at 2am + err = c.AddFunc("0 0 2 * * *", func() { updateOBIII(s, span.Context()) }) + if err != nil { + return err + } + + c.Start() + <-quit + c.Stop() + + return nil + }, + ) + +} diff --git a/commands/slash/update.go b/commands/slash/update.go new file mode 100644 index 0000000..f335d98 --- /dev/null +++ b/commands/slash/update.go @@ -0,0 +1,143 @@ +package slash + +import ( + "fmt" + + "github.com/bwmarrin/discordgo" + "github.com/sirupsen/logrus" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/commands/slash/permission" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/helpers" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/logging" + "gitlab.ritsec.cloud/1nv8rZim/ops-bot-iii/structs" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +func Update() *structs.SlashCommand { + return &structs.SlashCommand{ + Command: &discordgo.ApplicationCommand{ + Name: "update", + Description: "Update the bot", + DefaultMemberPermissions: &permission.Admin, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "force", + Description: "Force the bot to reboot", + Type: discordgo.ApplicationCommandOptionBoolean, + Required: false, + }, + }, + }, + Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { + span := tracer.StartSpan( + "commands.slash.update:Update", + tracer.ResourceName("/update"), + ) + defer span.Finish() + + logging.Debug(s, "Update command received", i.Member.User, span) + + force := false + if len(i.ApplicationCommandData().Options) != 0 { + force = i.ApplicationCommandData().Options[0].BoolValue() + } + + update, err := helpers.UpdateMainBranch() + if err != nil { + logging.Error(s, "Error updating main branch", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + + if !update { + logging.Debug(s, "No update available", i.Member.User, span) + + if !force { + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "No update available\nIf you want to force an update, use `/update force`", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + logging.Error(s, "Error responding to interaction", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + + return + } else { + logging.Debug(s, "Forcing update", i.Member.User, span) + + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "No update available; forcing update\nBot will be up temporarily once done updating", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + logging.Error(s, "Error responding to interaction", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + + err = helpers.BuildOBIII() + if err != nil { + logging.Error(s, "Error building OBIII", i.Member.User, span, logrus.Fields{"err": err.Error()}) + + content := fmt.Sprintf("Error building OBIII\n\nError:\n%s", err.Error()) + _, err = s.InteractionResponseEdit( + i.Interaction, + &discordgo.WebhookEdit{ + Content: &content, + }, + ) + if err != nil { + logging.Error(s, "Error editing interaction response", i.Member.User, span, logrus.Fields{"err": err.Error()}) + } + + return + } + + err = helpers.Exit() + if err != nil { + logging.Error(s, "Error exiting", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + + return + } + } + + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Update available; restarting now\nBot will be up temporarily once done updating", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + logging.Error(s, "Error responding to interaction", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + + err = helpers.BuildOBIII() + if err != nil { + content := fmt.Sprintf("Error building OBIII\n\nError:\n%s", err.Error()) + _, err = s.InteractionResponseEdit( + i.Interaction, + &discordgo.WebhookEdit{ + Content: &content, + }, + ) + + logging.Error(s, "Error building OBIII", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + + err = helpers.Exit() + if err != nil { + logging.Error(s, "Error exiting", i.Member.User, span, logrus.Fields{"err": err.Error()}) + return + } + }, + } +} diff --git a/go.sum b/go.sum index 5d557dd..88bac01 100644 --- a/go.sum +++ b/go.sum @@ -269,7 +269,6 @@ github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjp github.com/mailgun/mailgun-go v2.0.0+incompatible/go.mod h1:NWTyU+O4aczg/nsGhQnvHL6v2n5Gy6Sv5tNDVvC6FbU= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= @@ -284,7 +283,6 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= @@ -331,7 +329,6 @@ github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -634,7 +631,6 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/helpers/update.go b/helpers/update.go new file mode 100644 index 0000000..12cdda2 --- /dev/null +++ b/helpers/update.go @@ -0,0 +1,89 @@ +package helpers + +import ( + "bytes" + "fmt" + "os/exec" +) + +// UpdateMainBranch switches to the main branch, fetches from origin, pulls from origin, and returns true if an update was pulled +func UpdateMainBranch() (bool, error) { + switchCmd := exec.Command("git", "switch", "main") + + stderr := &bytes.Buffer{} + switchCmd.Stderr = stderr + + err := switchCmd.Run() + if err != nil { + return false, fmt.Errorf("error switching to main branch: %s", stderr.String()) + } + + fetchCmd := exec.Command("git", "fetch", "origin", "main") + + stderr = &bytes.Buffer{} + fetchCmd.Stderr = stderr + + err = fetchCmd.Run() + if err != nil { + return false, fmt.Errorf("error fetching from origin: %s", stderr.String()) + } + + commitCount := exec.Command("git", "rev-list", "--count", "HEAD...origin/main") + + stdout := &bytes.Buffer{} + stderr = &bytes.Buffer{} + + commitCount.Stdout = stdout + commitCount.Stderr = stderr + + err = commitCount.Run() + if err != nil { + return false, fmt.Errorf("error getting commit count: %s", stderr.String()) + } + + if stdout.String() == "0\n" { + return false, nil + } + + pullCmd := exec.Command("git", "pull", "origin", "main") + + stderr = &bytes.Buffer{} + pullCmd.Stderr = stderr + + err = pullCmd.Run() + if err != nil { + return false, fmt.Errorf("error pulling from origin: %s", stderr.String()) + } + + return true, nil +} + +// BuildOBIII builds the OBIII binary +func BuildOBIII() error { + buildCmd := exec.Command("/usr/local/go/bin/go", "build", "-o", "OBIII", "main.go") + + stderr := &bytes.Buffer{} + buildCmd.Stderr = stderr + + err := buildCmd.Run() + if err != nil { + return fmt.Errorf("error building obiii:\n%s\n%s", err.Error(), stderr.String()) + } + + return nil +} + +// Exit restarts the OBIII service +func Exit() error { + exitCmd := exec.Command("systemctl", "restart", "OBIII") + + stderr := &bytes.Buffer{} + exitCmd.Stderr = stderr + + err := exitCmd.Run() + if err != nil { + return fmt.Errorf("error restarting obiii: %s", stderr.String()) + } + + return nil +} diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 6f7d031..5c109d5 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/bash - +go env > OBIII.env cp OBIII.service /etc/systemd/system/OBIII.service systemctl daemon-reload systemctl stop OBIII