diff --git a/charts/argobot/Chart.yaml b/charts/argobot/Chart.yaml
index 6b76e49..18b46b6 100644
--- a/charts/argobot/Chart.yaml
+++ b/charts/argobot/Chart.yaml
@@ -2,5 +2,5 @@ apiVersion: v2
name: argobot
description: Helm chart for the corymurphy/argobot app
type: application
-version: 0.15.0
-appVersion: 0.15.0
+version: 0.16.0
+appVersion: 0.16.0
diff --git a/charts/argobot/templates/deployment.yaml b/charts/argobot/templates/deployment.yaml
index 6e6fab9..f54a5d9 100644
--- a/charts/argobot/templates/deployment.yaml
+++ b/charts/argobot/templates/deployment.yaml
@@ -63,6 +63,8 @@ spec:
readOnly: true
{{- end }}
env:
+ - name: ARGOBOT_LOG_LEVEL
+ value: {{ .Values.webServer.logLevel | default "info" }}
- name: ARGOBOT_GH_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
diff --git a/charts/argobot/values.yaml b/charts/argobot/values.yaml
index 75886ee..71d0983 100644
--- a/charts/argobot/values.yaml
+++ b/charts/argobot/values.yaml
@@ -48,6 +48,7 @@ config: |
argoCdUrl: http://argocd-server.argocd.svc.cluster.local:80
webServer:
+ logLevel: info
resources:
requests:
cpu: 250m
diff --git a/pkg/argocd/planner.go b/pkg/argocd/planner.go
new file mode 100644
index 0000000..15ae143
--- /dev/null
+++ b/pkg/argocd/planner.go
@@ -0,0 +1,48 @@
+package argocd
+
+import (
+ "context"
+
+ "github.com/corymurphy/argobot/pkg/logging"
+)
+
+type Planner struct {
+ ArgoClient *ApplicationsClient
+ Log logging.SimpleLogging
+}
+
+func NewPlanner(client *ApplicationsClient, log logging.SimpleLogging) *Planner {
+ return &Planner{
+ ArgoClient: client,
+ Log: log,
+ }
+}
+
+func (p *Planner) Plan(ctx context.Context, name string, revision string) (string, bool, error) {
+ var plan string
+ var diff bool = false
+
+ resources, err := p.ArgoClient.ManagedResources(name)
+
+ if err != nil {
+ return plan, diff, err
+ }
+
+ live, err := p.ArgoClient.Get(name)
+
+ if err != nil {
+ return plan, diff, err
+ }
+
+ target, err := p.ArgoClient.GetManifest(name, revision)
+ if err != nil {
+ return plan, diff, err
+ }
+
+ settings, err := p.ArgoClient.GetSettings()
+ if err != nil {
+ return plan, diff, err
+ }
+
+ return Plan(ctx, &settings, live, resources, target, revision, p.Log)
+}
diff --git a/pkg/assert/logger.go b/pkg/assert/logger.go
index b2adef6..1a7d5ca 100644
--- a/pkg/assert/logger.go
+++ b/pkg/assert/logger.go
@@ -3,6 +3,8 @@ package assert
import (
"fmt"
"testing"
+
+ "github.com/pkg/errors"
)
type TestLogger struct {
@@ -32,6 +34,10 @@ func (t *TestLogger) Warn(format string, a ...interface{}) {
t.log(format, a...)
}
-func (t *TestLogger) Err(format string, a ...interface{}) {
- t.log(format, a...)
+func (t *TestLogger) Err(err error, message string) {
+ t.t.Logf("%v", errors.Wrap(err, message))
}
+
+// func (t *TestLogger) Err(format string, a ...interface{}) {
+// t.log(format, a...)
+// }
diff --git a/pkg/cli/start.go b/pkg/cli/start.go
index f6c0f40..6f57ef7 100644
--- a/pkg/cli/start.go
+++ b/pkg/cli/start.go
@@ -12,6 +12,8 @@ import (
const (
WebhookSecretEnvVar = "ARGOBOT_GH_WEBHOOK_SECRET"
+ LogLevelEnvVar = "ARGOBOT_LOG_LEVEL"
+ DefaultLogLevel = logging.Info
)
var (
@@ -38,6 +40,14 @@ var run = &cobra.Command{
config.Github.App.PrivateKey = string(content)
config.Github.App.WebhookSecret = os.Getenv(WebhookSecretEnvVar)
+ logLevel := DefaultLogLevel
+ if serializedLevel, exists := os.LookupEnv(LogLevelEnvVar); exists {
+ logLevel, err = logging.GetLogLevel(serializedLevel)
+ if err != nil {
+ panic(err)
+ }
+ }
+
argoClient := &argocd.ApplicationsClient{
BaseUrl: config.ArgoCdUrl,
}
@@ -48,7 +58,7 @@ var run = &cobra.Command{
server.NewServer(
config,
- logging.NewLogger(logging.Info),
+ logging.NewLogger(logLevel),
argoClient,
).Start()
},
diff --git a/pkg/events/application_resolver.go b/pkg/events/application_resolver.go
index 1d8835b..f891f35 100644
--- a/pkg/events/application_resolver.go
+++ b/pkg/events/application_resolver.go
@@ -26,7 +26,7 @@ func NewApplicationResolver(githubClient *gogithub.Client, argocdClient *argocd.
}
}
-func (a *ApplicationResolver) FindApplicationNames(ctx context.Context, command *CommentCommand, event github.Event) ([]string, error) {
+func (a *ApplicationResolver) FindApplicationNames(ctx context.Context, event github.Event) ([]string, error) {
var changedApps []string
modified, err := a.GetModifiedFiles(ctx, event)
@@ -57,7 +57,7 @@ func (a *ApplicationResolver) FindApplicationNames(ctx context.Context, command
// copied from atlantis
func (a *ApplicationResolver) GetModifiedFiles(ctx context.Context, event github.Event) ([]string, error) {
- a.Log.Debug("Getting modified files for GitHub pull request %d")
+ a.Log.Debug("Getting modified files for GitHub pull request")
var files []string
nextPage := 0
diff --git a/pkg/events/apply_runner.go b/pkg/events/apply_runner.go
index 6bdfccf..a7893d3 100644
--- a/pkg/events/apply_runner.go
+++ b/pkg/events/apply_runner.go
@@ -28,7 +28,7 @@ func NewApplyRunner(vcsClient *github.Client, config *env.Config, log logging.Si
}
// TODO: validate that the PR is in an approved/mergeable state
-func (a *ApplyRunner) Run(ctx context.Context, cmd *CommentCommand, event vsc.Event) (CommentResponse, error) {
+func (a *ApplyRunner) Run(ctx context.Context, app string, event vsc.Event) (CommentResponse, error) {
var resp CommentResponse
status, err := vsc.NewPullRequestStatusFetcher(ctx, a.Log, a.vcsClient).Fetch(event)
@@ -43,7 +43,7 @@ func (a *ApplyRunner) Run(ctx context.Context, cmd *CommentCommand, event vsc.Ev
return NewCommentResponse("pull request must be approved and in a mergeable state", event), nil
}
- apply, err := a.ApplyClient.Apply(cmd.Application, event.Revision)
+ apply, err := a.ApplyClient.Apply(app, event.Revision)
if err != nil {
return resp, fmt.Errorf("argoclient failed while applying %w", err)
}
diff --git a/pkg/events/comment_command.go b/pkg/events/comment_command.go
index 58090fe..112f11f 100644
--- a/pkg/events/comment_command.go
+++ b/pkg/events/comment_command.go
@@ -2,9 +2,18 @@ package events
import "github.com/corymurphy/argobot/pkg/command"
+func NewAutoRunCommand(apps []string, name command.Name) *CommentCommand {
+ return &CommentCommand{
+ Flags: []string{},
+ Name: name,
+ Applications: apps,
+ ExplicitApplication: false,
+ }
+}
+
type CommentCommand struct {
- Flags []string
- Name command.Name
- Application string
- Applications []string
+ Flags []string
+ Name command.Name
+ Applications []string
+ ExplicitApplication bool
}
diff --git a/pkg/events/comment_handler.go b/pkg/events/comment_handler.go
deleted file mode 100644
index 3831117..0000000
--- a/pkg/events/comment_handler.go
+++ /dev/null
@@ -1,194 +0,0 @@
-package events
-
-import (
- "context"
- "fmt"
-
- "github.com/argoproj/argo-cd/v2/pkg/apiclient/application"
- "github.com/corymurphy/argobot/pkg/argocd"
- "github.com/corymurphy/argobot/pkg/command"
- "github.com/corymurphy/argobot/pkg/env"
- vsc "github.com/corymurphy/argobot/pkg/github"
- "github.com/corymurphy/argobot/pkg/logging"
- "github.com/corymurphy/argobot/pkg/utils"
- "github.com/google/go-github/v53/github"
- "github.com/palantir/go-githubapp/githubapp"
- "github.com/pkg/errors"
-)
-
-const maxCommentLength = 32768
-
-type PRCommentHandler struct {
- githubapp.ClientCreator
- Config *env.Config
- Log logging.SimpleLogging
- ArgoClient argocd.ApplicationsClient
-}
-
-func (h *PRCommentHandler) Handles() []string {
- return []string{"issue_comment", "pull_request"}
-}
-
-func (h *PRCommentHandler) Handle(ctx context.Context, eventType string, deliveryID string, payload []byte) error {
- // var event github.IssueCommentEvent
- // event.GetInstallation()
- // var pr github.PullRequestEvent
- event, err := vsc.NewEvent(eventType, payload)
- if err != nil {
- h.Log.Err(err, "unable to parse event metadata")
- return nil
- }
-
- comment := NewCommentParser(h.Log).Parse(event)
- if (comment.Ignore || comment.ImmediateResponse) && !comment.HasResponseComment {
- return nil
- }
-
- installationID := githubapp.GetInstallationIDFromEvent(&event)
-
- client, err := h.NewInstallationClient(installationID)
- if err != nil {
- return err
- }
-
- pr, _, err := client.PullRequests.Get(ctx, event.Repository.Owner, event.Repository.Name, event.PullRequest.Number)
- if err != nil {
- return err
- }
-
- // TODO: this should happen in NewEvent
- event.Revision = *pr.GetHead().SHA
-
- if (comment.Ignore || comment.ImmediateResponse) && comment.HasResponseComment {
- prComment := github.IssueComment{
- Body: &comment.CommentResponse,
- }
-
- if _, _, err := client.Issues.CreateComment(ctx, event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, &prComment); err != nil {
- return err
- }
- }
- // TODO: add reaction like this
- // err := e.VCSClient.ReactToComment(logger, baseRepo, pullNum, commentID, e.EmojiReaction)
-
- // TODO: apply and plan async and respond to hook immediately
-
- // TODO: move this to startup and cache?
-
- if comment.Command.Application == "" {
- appResolver := NewApplicationResolver(client, &h.ArgoClient, h.Log)
- apps, err := appResolver.FindApplicationNames(ctx, comment.Command, event)
- if err != nil {
- h.Log.Err(err, "unable to resolve app name")
- return nil
- }
-
- comment.Command.Applications = apps
- }
-
- if comment.Command.Name == command.Plan {
- for _, app := range comment.Command.Applications {
- var err error = nil
-
- plan, diff, err := h.Plan(ctx, app, event.Revision)
- if err != nil {
- h.Log.Err(err, fmt.Sprintf("unable to plan: %s", plan))
- return err
- }
- var msg string
- if diff {
- msg = fmt.Sprintf("argocd plan for `%s`\n\n", app) + "```diff\n" + plan + "\n```"
- } else {
- msg = "no diff detected, current state is up to date with this revision."
- h.Log.Info(plan)
- }
-
- err = h.CreateComment(client, ctx, event, msg, comment.Command.Name.String())
- if err != nil {
- h.Log.Err(err, fmt.Sprintf("error while planning %s", app))
- }
- }
- return nil
- }
-
- // TODO allow autoapply
- if comment.Command.Name == command.Apply {
- go func() {
- apply := NewApplyRunner(client, h.Config, h.Log, &h.ArgoClient)
- response, err := apply.Run(ctx, comment.Command, event)
- if err != nil {
- h.Log.Err(err, "unable to apply")
-
- return
- }
- msg := fmt.Sprintf("apply result for `%s`\n\n", comment.Command.Application) + "```\n" + response.Message + "\n```"
- h.CreateComment(client, ctx, event, msg, comment.Command.Name.String())
- }()
- return nil
- }
-
- return errors.Errorf("unsupported argo command")
-}
-
-// TODO: this is just temporary while i build the proof of concept
-func (h *PRCommentHandler) Plan(ctx context.Context, name string, revision string) (string, bool, error) {
- var plan string
- var diff bool = false
- var resources *application.ManagedResourcesResponse
-
- resources, err := h.ArgoClient.ManagedResources(name)
-
- if err != nil {
- return plan, diff, err
- }
-
- live, err := h.ArgoClient.Get(name)
-
- if err != nil {
- return plan, diff, err
- }
-
- target, err := h.ArgoClient.GetManifest(name, revision)
- if err != nil {
- return plan, diff, err
- }
-
- settings, err := h.ArgoClient.GetSettings()
- if err != nil {
- return plan, diff, err
- }
-
- return argocd.Plan(ctx, &settings, live, resources, target, revision, h.Log)
-}
-
-// TODO move this to another module
-func (h *PRCommentHandler) CreateComment(client *github.Client, ctx context.Context, event vsc.Event, comment string, command string) error {
- h.Log.Debug("Creating comment on GitHub pull request %d", event.PullRequest.Number)
- var sepStart string
-
- sepEnd := "\n```\n" +
- "\n
\n\n**Warning**: Output length greater than max comment size. Continued in next comment."
-
- if command != "" {
- sepStart = fmt.Sprintf("Continued %s output from previous comment.\nShow Output
\n\n", command) +
- "```diff\n"
- } else {
- sepStart = "Continued from previous comment.\nShow Output
\n\n" +
- "```diff\n"
- }
-
- truncationHeader := "\n```\n " +
- "\n
\n\n**Warning**: Command output is larger than the maximum number of comments per command. Output truncated.\n\n[..]\n"
-
- comments := utils.SplitComment(comment, maxCommentLength, sepEnd, sepStart, 100, truncationHeader)
- for i := range comments {
- _, resp, err := client.Issues.CreateComment(ctx, event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, &github.IssueComment{Body: &comments[i]})
- if resp != nil {
- h.Log.Debug("POST /repos/%v/%v/issues/%d/comments returned: %v", event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, resp.StatusCode)
- }
- if err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/pkg/events/comment_handler_test.go b/pkg/events/comment_handler_test.go
deleted file mode 100644
index b3adf69..0000000
--- a/pkg/events/comment_handler_test.go
+++ /dev/null
@@ -1 +0,0 @@
-package events
diff --git a/pkg/events/comment_parser.go b/pkg/events/comment_parser.go
index eebf0ac..878b7ce 100644
--- a/pkg/events/comment_parser.go
+++ b/pkg/events/comment_parser.go
@@ -176,9 +176,9 @@ func (c *CommentParser) Parse(event vsc.Event) *CommentParseResult {
return &CommentParseResult{
Command: &CommentCommand{
- Name: name,
- Application: app,
- Applications: []string{app},
+ Name: name,
+ Applications: []string{app},
+ ExplicitApplication: true,
},
}
}
diff --git a/pkg/events/comment_parser_test.go b/pkg/events/comment_parser_test.go
index b5f6be2..b60566d 100644
--- a/pkg/events/comment_parser_test.go
+++ b/pkg/events/comment_parser_test.go
@@ -1,11 +1,13 @@
package events
import (
+ "encoding/json"
"testing"
"github.com/corymurphy/argobot/pkg/command"
- "github.com/corymurphy/argobot/pkg/github"
+ vsc "github.com/corymurphy/argobot/pkg/github"
"github.com/corymurphy/argobot/pkg/logging"
+ "github.com/google/go-github/v53/github"
)
func Test_Comment_IsHelp(t *testing.T) {
@@ -25,10 +27,9 @@ func Test_Comment_IsHelp(t *testing.T) {
}
}
`
- event, err := github.NewEvent("issue_comment", []byte(serialized))
- if err != nil {
- t.Error(err)
- }
+ var comment github.IssueCommentEvent
+ json.Unmarshal([]byte(serialized), &comment)
+ event := vsc.InitializeFromIssueComment(comment, "")
parser := NewCommentParser(logging.NewLogger(logging.Silent))
result := parser.Parse(event)
@@ -54,10 +55,9 @@ func Test_Comment_IsBot(t *testing.T) {
}
}
`
- event, err := github.NewEvent("issue_comment", []byte(serialized))
- if err != nil {
- t.Error(err)
- }
+ var comment github.IssueCommentEvent
+ json.Unmarshal([]byte(serialized), &comment)
+ event := vsc.InitializeFromIssueComment(comment, "")
parser := NewCommentParser(logging.NewLogger(logging.Silent))
result := parser.Parse(event)
@@ -85,10 +85,9 @@ func Test_PlanHasApplicationName(t *testing.T) {
}
`
- event, err := github.NewEvent("issue_comment", []byte(serialized))
- if err != nil {
- t.Error(err)
- }
+ var comment github.IssueCommentEvent
+ json.Unmarshal([]byte(serialized), &comment)
+ event := vsc.InitializeFromIssueComment(comment, "")
parser := NewCommentParser(logging.NewLogger(logging.Silent))
result := parser.Parse(event)
@@ -96,9 +95,9 @@ func Test_PlanHasApplicationName(t *testing.T) {
t.Errorf("expected %s, got %s", command.Help, &result.Command.Name)
}
- if result.Command.Application != "myapp" {
+ if result.Command.Applications[0] != "myapp" {
t.Log(result.Command)
- t.Errorf("expected %s, got %s", "myapp", result.Command.Application)
+ t.Errorf("expected %s, got %s", "myapp", result.Command.Applications[0])
}
}
@@ -120,10 +119,9 @@ func Test_ApplyHasApplicationName(t *testing.T) {
}
`
- event, err := github.NewEvent("issue_comment", []byte(serialized))
- if err != nil {
- t.Error(err)
- }
+ var comment github.IssueCommentEvent
+ json.Unmarshal([]byte(serialized), &comment)
+ event := vsc.InitializeFromIssueComment(comment, "")
parser := NewCommentParser(logging.NewLogger(logging.Silent))
result := parser.Parse(event)
@@ -131,8 +129,8 @@ func Test_ApplyHasApplicationName(t *testing.T) {
t.Errorf("expected %s, got %s", command.Apply, &result.Command.Name)
}
- if result.Command.Application != "myapp" {
+ if result.Command.Applications[0] != "myapp" {
t.Log(result.Command)
- t.Errorf("expected %s, got %s", "myapp", result.Command.Application)
+ t.Errorf("expected %s, got %s", "myapp", result.Command.Applications[0])
}
}
diff --git a/pkg/events/issue_comment_handler.go b/pkg/events/issue_comment_handler.go
new file mode 100644
index 0000000..f545111
--- /dev/null
+++ b/pkg/events/issue_comment_handler.go
@@ -0,0 +1,134 @@
+package events
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/corymurphy/argobot/pkg/argocd"
+ "github.com/corymurphy/argobot/pkg/command"
+ "github.com/corymurphy/argobot/pkg/env"
+ vsc "github.com/corymurphy/argobot/pkg/github"
+ "github.com/corymurphy/argobot/pkg/logging"
+ "github.com/google/go-github/v53/github"
+ "github.com/palantir/go-githubapp/githubapp"
+ "github.com/pkg/errors"
+)
+
+var validIssueCommentActions = []string{"created"}
+
+type IssueCommentHandler struct {
+ githubapp.ClientCreator
+ Config *env.Config
+ Log logging.SimpleLogging
+ ArgoClient argocd.ApplicationsClient
+}
+
+func (h *IssueCommentHandler) Handles() []string {
+ return []string{"issue_comment"}
+}
+
+func (h *IssueCommentHandler) Handle(ctx context.Context, eventType string, deliveryID string, payload []byte) error {
+ var issue github.IssueCommentEvent
+
+ if err := json.Unmarshal(payload, &issue); err != nil {
+ h.Log.Err(err, "invalid github event payload")
+ return fmt.Errorf("invalid github event payload")
+ }
+
+ installationID := githubapp.GetInstallationIDFromEvent(&issue)
+ client, err := h.NewInstallationClient(installationID)
+ if err != nil {
+ return err
+ }
+
+ pr, _, err := client.PullRequests.Get(ctx, issue.GetRepo().GetOwner().GetLogin(), issue.GetRepo().GetName(), issue.Issue.GetNumber())
+ if err != nil {
+ h.Log.Err(err, "unable to get revision from pull request")
+ return nil
+ }
+ event := vsc.InitializeFromIssueComment(issue, *pr.GetHead().SHA)
+
+ comment := NewCommentParser(h.Log).Parse(event)
+ if (comment.Ignore || comment.ImmediateResponse) && !comment.HasResponseComment {
+ h.Log.Debug("ignoring comment")
+ return nil
+ }
+
+ if (comment.Ignore || comment.ImmediateResponse) && comment.HasResponseComment {
+ prComment := github.IssueComment{
+ Body: &comment.CommentResponse,
+ }
+
+ if _, _, err := client.Issues.CreateComment(ctx, event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, &prComment); err != nil {
+ return err
+ }
+ }
+
+ var apps []string
+ if comment.Command.ExplicitApplication {
+ apps = comment.Command.Applications
+ } else {
+ resolver := NewApplicationResolver(client, &h.ArgoClient, h.Log)
+ apps, err = resolver.FindApplicationNames(ctx, event)
+ if err != nil {
+ h.Log.Debug("pull request did not change any applications managed by argocd")
+ return nil
+ }
+ }
+
+ commenter := vsc.NewCommenter(client, h.Log, ctx)
+
+ if comment.Command.Name == command.Plan {
+ planner := argocd.NewPlanner(&h.ArgoClient, h.Log)
+
+ for _, app := range apps {
+ var err error = nil
+
+ plan, diff, err := planner.Plan(ctx, app, event.Revision)
+ if err != nil {
+ h.Log.Err(err, fmt.Sprintf("unable to plan: %s", plan))
+ return err
+ }
+ var comment string
+ if diff {
+ comment = fmt.Sprintf("argocd plan for `%s`\n\n", app) + "```diff\n" + plan + "\n```"
+ } else {
+ comment = "no diff detected, current state is up to date with this revision."
+ h.Log.Info(plan)
+ }
+
+ commenter.Plan(&event, app, command.Plan.String(), comment)
+ if err != nil {
+ h.Log.Err(err, fmt.Sprintf("error while planning %s", app))
+ }
+ }
+ return nil
+ }
+
+ if comment.Command.Name == command.Apply {
+ // TODO: allow for multiple apps to be applied, I want to be careful about this
+ if len(apps) != 1 {
+ h.Log.Info("requested apply with more than 1 app, only one app allowed when applying")
+ return nil
+ }
+ go func() {
+ for _, app := range apps {
+ applyContext, cancel := context.WithTimeout(ctx, 60*time.Second)
+ defer cancel()
+ apply := NewApplyRunner(client, h.Config, h.Log, &h.ArgoClient)
+ response, err := apply.Run(applyContext, app, event)
+ if err != nil {
+ h.Log.Err(err, "unable to apply")
+ return
+ }
+ comment := fmt.Sprintf("apply result for `%s`\n\n", app) + "```\n" + response.Message + "\n```"
+ commenter.Comment(&event, &comment)
+ }
+ }()
+ return nil
+ }
+
+ return errors.Errorf("unsupported argo command")
+}
diff --git a/pkg/events/pull_request_handler.go b/pkg/events/pull_request_handler.go
new file mode 100644
index 0000000..54f6142
--- /dev/null
+++ b/pkg/events/pull_request_handler.go
@@ -0,0 +1,81 @@
+package events
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/corymurphy/argobot/pkg/argocd"
+ "github.com/corymurphy/argobot/pkg/command"
+ "github.com/corymurphy/argobot/pkg/env"
+ vsc "github.com/corymurphy/argobot/pkg/github"
+ "github.com/corymurphy/argobot/pkg/logging"
+ "github.com/corymurphy/argobot/pkg/utils"
+ "github.com/google/go-github/v53/github"
+ "github.com/palantir/go-githubapp/githubapp"
+ "github.com/pkg/errors"
+)
+
+var validPullActions = []string{"opened", "reopened", "ready_for_review"}
+
+type PullRequestHandler struct {
+ githubapp.ClientCreator
+ Config *env.Config
+ Log logging.SimpleLogging
+ ArgoClient argocd.ApplicationsClient
+}
+
+func (h *PullRequestHandler) Handles() []string {
+ return []string{"pull_request"}
+}
+
+func (h *PullRequestHandler) Handle(ctx context.Context, eventType string, deliveryID string, payload []byte) error {
+
+ var pull github.PullRequestEvent
+ if err := json.Unmarshal(payload, &pull); err != nil {
+ h.Log.Err(err, "invalid github event payload")
+ return fmt.Errorf("invalid github event payload")
+ }
+
+ if !utils.StringInSlice(*pull.Action, validPullActions) {
+ h.Log.Debug("ignoring pull request action %s", *pull.Action)
+ return nil
+ }
+
+ event := vsc.InitializeFromPullRequest(pull)
+ installationID := githubapp.GetInstallationIDFromEvent(&pull)
+ client, err := h.NewInstallationClient(installationID)
+ if err != nil {
+ return err
+ }
+
+ resolver := NewApplicationResolver(client, &h.ArgoClient, h.Log)
+ apps, err := resolver.FindApplicationNames(ctx, event)
+ if err != nil {
+ h.Log.Debug("pull request did not change any applications managed by argocd")
+ return nil
+ }
+
+ commenter := vsc.NewCommenter(client, h.Log, ctx)
+ planner := argocd.NewPlanner(&h.ArgoClient, h.Log)
+
+ for _, app := range apps {
+
+ plan, diff, err := planner.Plan(ctx, app, event.Revision)
+ if err != nil {
+ h.Log.Err(err, fmt.Sprintf("unable to plan: %s", plan))
+ return err
+ }
+
+ var comment string
+ if diff {
+ comment = fmt.Sprintf("argocd plan for `%s`\n\n", app) + "```diff\n" + plan + "\n```"
+ } else {
+ comment = "no diff detected, current state is up to date with this revision."
+ }
+
+ commenter.Plan(&event, app, command.Plan.String(), comment)
+ }
+
+ return errors.Errorf("unsupported argo command")
+}
diff --git a/pkg/github/commenter.go b/pkg/github/commenter.go
new file mode 100644
index 0000000..757e0f8
--- /dev/null
+++ b/pkg/github/commenter.go
@@ -0,0 +1,69 @@
+package github
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/corymurphy/argobot/pkg/logging"
+ "github.com/corymurphy/argobot/pkg/utils"
+ "github.com/google/go-github/v53/github"
+)
+
+const maxCommentLength = 32768
+
+type Commenter struct {
+ client *github.Client
+ log logging.SimpleLogging
+ ctx context.Context
+}
+
+func NewCommenter(client *github.Client, log logging.SimpleLogging, ctx context.Context) *Commenter {
+ return &Commenter{
+ client: client,
+ log: log,
+ ctx: ctx,
+ }
+}
+
+func (c *Commenter) Plan(event *Event, app string, command string, comment string) error {
+ c.log.Debug("Creating comment on GitHub pull request %d", event.PullRequest.Number)
+ var sepStart string
+
+ sepEnd := "\n```\n " +
+ "\n
\n\n**Warning**: Output length greater than max comment size. Continued in next comment."
+
+ if command != "" {
+ sepStart = fmt.Sprintf("Continued %s output from previous comment.\nShow Output
\n\n", command) +
+ "```diff\n"
+ } else {
+ sepStart = "Continued from previous comment.\nShow Output
\n\n" +
+ "```diff\n"
+ }
+
+ truncationHeader := "\n```\n " +
+ "\n
\n\n**Warning**: Command output is larger than the maximum number of comments per command. Output truncated.\n\n[..]\n"
+
+ comments := utils.SplitComment(comment, maxCommentLength, sepEnd, sepStart, 100, truncationHeader)
+ for i := range comments {
+ _, resp, err := c.client.Issues.CreateComment(c.ctx, event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, &github.IssueComment{Body: &comments[i]})
+ if resp != nil {
+ c.log.Debug("POST /repos/%v/%v/issues/%d/comments returned: %v", event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, resp.StatusCode)
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Commenter) Comment(event *Event, comment *string) error {
+ _, resp, err := c.client.Issues.CreateComment(c.ctx, event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, &github.IssueComment{Body: comment})
+ if resp != nil {
+ c.log.Debug("POST /repos/%v/%v/issues/%d/comments returned: %v", event.Repository.Owner, event.Repository.Name, event.PullRequest.Number, resp.StatusCode)
+ }
+ if err != nil {
+ return err
+ }
+ return err
+}
diff --git a/pkg/github/event_metadata.go b/pkg/github/event_metadata.go
index 5440531..4d8f57c 100644
--- a/pkg/github/event_metadata.go
+++ b/pkg/github/event_metadata.go
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/google/go-github/v53/github"
+ "github.com/palantir/go-githubapp/githubapp"
"github.com/pkg/errors"
)
@@ -55,6 +56,7 @@ type Event struct {
Repository Repository
PullRequest PullRequest
InstallationProvider InstallationProvider
+ GithubClient *github.Client
}
func (e *Event) GetInstallation() *github.Installation {
@@ -65,7 +67,7 @@ func (e *Event) HasMessage() bool {
return e.Message != ""
}
-func NewEvent(eventType string, payload []byte) (Event, error) {
+func NewEvent(clientCreator githubapp.ClientCreator, eventType string, payload []byte) (Event, error) {
var event Event
var githubEvent GithubEvent
if err := json.Unmarshal(payload, &githubEvent); err != nil {
@@ -77,6 +79,10 @@ func NewEvent(eventType string, payload []byte) (Event, error) {
if err := json.Unmarshal(payload, &comment); err != nil {
return event, errors.Wrap(err, "failed to parse issue comment event payload")
}
+ // comment.
+
+ // comment.GetChanges().Base.SHA.From
+ // comment.Re
return Event{
Actor: Actor{Name: comment.GetComment().GetUser().GetLogin()},
@@ -104,6 +110,7 @@ func NewEvent(eventType string, payload []byte) (Event, error) {
Actor: Actor{Name: pr.GetPullRequest().GetUser().GetLogin()},
Action: Opened,
IsPullRequest: true,
+ Revision: *pr.PullRequest.Head.SHA,
Repository: Repository{
Name: pr.GetRepo().GetName(),
Owner: pr.GetRepo().GetOwner().GetLogin(),
@@ -118,3 +125,38 @@ func NewEvent(eventType string, payload []byte) (Event, error) {
return event, fmt.Errorf("unsupported event %s %s", eventType, *githubEvent.Action)
}
+
+func InitializeFromIssueComment(source github.IssueCommentEvent, revision string) Event {
+ return Event{
+ Actor: Actor{Name: source.GetComment().GetUser().GetLogin()},
+ Action: Comment,
+ IsPullRequest: source.GetIssue().IsPullRequest(),
+ Revision: revision,
+ Repository: Repository{
+ Name: source.GetRepo().GetName(),
+ Owner: source.GetRepo().GetOwner().GetLogin(),
+ },
+ PullRequest: PullRequest{
+ Number: source.GetIssue().GetNumber(),
+ },
+ Message: *source.GetComment().Body,
+ InstallationProvider: &source,
+ }
+}
+
+func InitializeFromPullRequest(source github.PullRequestEvent) Event {
+ return Event{
+ Actor: Actor{Name: source.GetPullRequest().GetUser().GetLogin()},
+ Action: Opened,
+ IsPullRequest: true,
+ Revision: *source.PullRequest.Head.SHA,
+ Repository: Repository{
+ Name: source.GetRepo().GetName(),
+ Owner: source.GetRepo().GetOwner().GetLogin(),
+ },
+ PullRequest: PullRequest{
+ Number: source.GetPullRequest().GetNumber(),
+ },
+ Message: "",
+ }
+}
diff --git a/pkg/logging/simple_logging.go b/pkg/logging/simple_logging.go
index 3050c17..ce75208 100644
--- a/pkg/logging/simple_logging.go
+++ b/pkg/logging/simple_logging.go
@@ -1,6 +1,7 @@
package logging
import (
+ "fmt"
"log"
"github.com/pkg/errors"
@@ -25,6 +26,26 @@ type Logger struct {
level int
}
+func GetLogLevel(input string) (int, error) {
+ // level, err := strconv.Atoi(input)
+ // if err != nil {
+ // return level, err
+ // }
+ switch input {
+ case "silent":
+ return Silent, nil
+ case "error":
+ return Err, nil
+ case "warn":
+ return Warn, nil
+ case "info":
+ return Info, nil
+ case "debug":
+ return Debug, nil
+ }
+ return Info, fmt.Errorf("invalid log level %s", input)
+}
+
func NewLogger(level int) *Logger {
return &Logger{
level: level,
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 786431e..d0a1d32 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -14,12 +14,12 @@ import (
)
type Server struct {
- Config *env.Config
- Log logging.SimpleLogging
- PRCommentHandler http.Handler
- PlanClient argocd.PlanClient
- ApplyClient argocd.ApplyClient
- ArgoClient argocd.ApplicationsClient
+ Config *env.Config
+ Log logging.SimpleLogging
+ WebhookHandler http.Handler
+ PlanClient argocd.PlanClient
+ ApplyClient argocd.ApplyClient
+ ArgoClient argocd.ApplicationsClient
githubapp.ClientCreator
}
@@ -37,20 +37,27 @@ func NewServer(config *env.Config, logger logging.SimpleLogging, argoClient *arg
panic(err)
}
- prCommentHandler := &events.PRCommentHandler{
+ issueCommentHandler := &events.IssueCommentHandler{
ClientCreator: cc,
Config: config,
Log: logger,
ArgoClient: *argoClient,
}
+ pullRequestHandler := &events.PullRequestHandler{
+ ClientCreator: cc,
+ Config: config,
+ Log: logger,
+ ArgoClient: *argoClient,
+ }
+
+ webhookHandler := githubapp.NewDefaultEventDispatcher(config.Github, issueCommentHandler, pullRequestHandler)
- webhookHandler := githubapp.NewDefaultEventDispatcher(config.Github, prCommentHandler)
return &Server{
- Config: config,
- Log: logger,
- ClientCreator: cc,
- PRCommentHandler: webhookHandler,
- ArgoClient: *argoClient,
+ Config: config,
+ Log: logger,
+ ClientCreator: cc,
+ WebhookHandler: webhookHandler,
+ ArgoClient: *argoClient,
}
}
@@ -64,7 +71,7 @@ func (s *Server) Health(w http.ResponseWriter, r *http.Request) {
func (s *Server) Start() {
http.Handle("/health", http.HandlerFunc(s.Health))
- http.Handle(githubapp.DefaultWebhookRoute, s.PRCommentHandler)
+ http.Handle(githubapp.DefaultWebhookRoute, s.WebhookHandler)
addr := fmt.Sprintf("%s:%d", s.Config.Server.Address, s.Config.Server.Port)
s.Log.Info("starting server on %s", addr)
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 418a8bc..786ccb3 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -77,7 +77,7 @@ func Test_PRCommentHandler(t *testing.T) {
req := NewWebhookRequest(testCase.BodyPath, testCase.Config)
- s.PRCommentHandler.ServeHTTP(w, req)
+ s.WebhookHandler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Result().StatusCode)
}
diff --git a/pkg/utils/lists.go b/pkg/utils/lists.go
new file mode 100644
index 0000000..bcde9f5
--- /dev/null
+++ b/pkg/utils/lists.go
@@ -0,0 +1,10 @@
+package utils
+
+func StringInSlice(a string, list []string) bool {
+ for _, b := range list {
+ if b == a {
+ return true
+ }
+ }
+ return false
+}
diff --git a/test/dev/install_test.go b/test/dev/install_test.go
index 580e98a..31dd73c 100644
--- a/test/dev/install_test.go
+++ b/test/dev/install_test.go
@@ -44,8 +44,9 @@ func Test_LocalDevelopmentInstall(t *testing.T) {
options := &helm.Options{
KubectlOptions: kubectlOptions,
SetValues: map[string]string{
- "chartVersion": strings.TrimSpace(version),
- "image.tag": tag,
+ "chartVersion": strings.TrimSpace(version),
+ "image.tag": tag,
+ "webServer.logLevel": "debug",
},
ExtraArgs: map[string][]string{
"upgrade": {"--timeout", "15s", "--install", "--wait-for-jobs", "--wait", "--create-namespace", "--namespace", namespace},
diff --git a/version b/version
index a551051..04a373e 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.15.0
+0.16.0