Skip to content

Commit

Permalink
Add GitHub issue creator (#953)
Browse files Browse the repository at this point in the history
  • Loading branch information
mszostok authored Jan 27, 2023
1 parent 7d26b6d commit 3ebc48e
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .goreleaser.plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ builds:
goarch: *goarch
goarm: *goarm

- id: gh
main: cmd/executor/gh/main.go
binary: executor_gh_{{ .Os }}_{{ .Arch }}

no_unique_dist_dir: true
env: *env
goos: *goos
goarch: *goarch
goarm: *goarm

- id: cm-watcher
main: cmd/source/cm-watcher/main.go
binary: source_cm-watcher_{{ .Os }}_{{ .Arch }}
Expand Down
211 changes: 211 additions & 0 deletions cmd/executor/gh/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package main

import (
"bytes"
"context"
"fmt"
"log"
"text/template"

"github.com/hashicorp/go-plugin"

"github.com/kubeshop/botkube/pkg/api"
"github.com/kubeshop/botkube/pkg/api/executor"
"github.com/kubeshop/botkube/pkg/pluginx"
)

const (
pluginName = "gh"
logsTailLines = 150
defaultNamespace = "default"
)

// version is set via ldflags by GoReleaser.
var version = "dev"

// Config holds the GitHub executor configuration.
type Config struct {
GitHub struct {
Token string
Repository string
IssueTemplate string
}
}

// Commands defines all supported GitHub plugin commands and their flags.
type (
Commands struct {
Create *CreateCommand `arg:"subcommand:create"`
}
CreateCommand struct {
Issue *CreateIssueCommand `arg:"subcommand:issue"`
}
CreateIssueCommand struct {
Type string `arg:"positional"`
Namespace string `arg:"-n,--namespace"`
}
)

// GHExecutor implements the Botkube executor plugin interface.
type GHExecutor struct{}

// Metadata returns details about the GitHub plugin.
func (*GHExecutor) Metadata(context.Context) (api.MetadataOutput, error) {
return api.MetadataOutput{
Version: version,
Description: "GH creates an issue on GitHub for a related Kubernetes resource.",
}, nil
}

// Execute returns a given command as a response.
func (e *GHExecutor) Execute(ctx context.Context, in executor.ExecuteInput) (executor.ExecuteOutput, error) {
var cfg Config
err := pluginx.MergeExecutorConfigs(in.Configs, &cfg)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while merging input configs: %w", err)
}

var cmd Commands
err = pluginx.ParseCommand(pluginName, in.Command, &cmd)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while parsing input command: %w", err)
}

if cmd.Create == nil || cmd.Create.Issue == nil {
return executor.ExecuteOutput{
Data: fmt.Sprintf("Usage: %s create issue KIND/NAME", pluginName),
}, nil
}

issueDetails, err := getIssueDetails(ctx, cmd.Create.Issue.Namespace, cmd.Create.Issue.Type)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while fetching logs : %w", err)
}

mdBody, err := renderIssueBody(cfg.GitHub.IssueTemplate, issueDetails)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while rendering issue body: %w", err)
}

title := fmt.Sprintf("The `%s` malfunctions", cmd.Create.Issue.Type)
issueURL, err := createGitHubIssue(cfg, title, mdBody)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while creating GitHub issue: %w", err)
}

return executor.ExecuteOutput{
Data: fmt.Sprintf("New issue created successfully! 🎉\n\nIssue URL: %s", issueURL),
}, nil
}

var depsDownloadLinks = map[string]api.Dependency{
// Links source: https://github.com/cli/cli/releases/tag/v2.21.2
"gh": {
URLs: map[string]string{
// Using go-getter syntax to unwrap the underlying directory structure.
// Read more on https://github.com/hashicorp/go-getter#subdirectories
"darwin/amd64": "https://github.com/cli/cli/releases/download/v2.21.2/gh_2.21.2_macOS_amd64.tar.gz//gh_2.21.2_macOS_amd64/bin",
"linux/amd64": "https://github.com/cli/cli/releases/download/v2.21.2/gh_2.21.2_linux_amd64.tar.gz//gh_2.21.2_linux_amd64/bin",
"linux/arm64": "https://github.com/cli/cli/releases/download/v2.21.2/gh_2.21.2_linux_arm64.tar.gz//gh_2.21.2_linux_arm64/bin",
"linux/386": "https://github.com/cli/cli/releases/download/v2.21.2/gh_2.21.2_linux_386.tar.gz//gh_2.21.2_linux_386/bin",
},
},
"kubectl": {
URLs: map[string]string{
"darwin/amd64": "https://dl.k8s.io/release/v1.26.0/bin/darwin/amd64/kubectl",
"linux/amd64": "https://dl.k8s.io/release/v1.26.0/bin/linux/amd64/kubectl",
"linux/arm64": "https://dl.k8s.io/release/v1.26.0/bin/linux/arm64/kubectl",
"linux/386": "https://dl.k8s.io/release/v1.26.0/bin/linux/386/kubectl",
},
},
}

func main() {
err := pluginx.DownloadDependencies(depsDownloadLinks)
if err != nil {
log.Fatal(err)
}

executor.Serve(map[string]plugin.Plugin{
pluginName: &executor.Plugin{
Executor: &GHExecutor{},
},
})
}

func createGitHubIssue(cfg Config, title, mdBody string) (string, error) {
cmd := fmt.Sprintf("gh issue create --title %q --body '%s' --label bug -R %s", title, mdBody, cfg.GitHub.Repository)

envs := map[string]string{
"GH_TOKEN": cfg.GitHub.Token,
}

return pluginx.ExecuteCommandWithEnvs(context.Background(), cmd, envs)
}

// IssueDetails holds all available information about a given issue.
type IssueDetails struct {
Type string
Namespace string
Logs string
Version string
}

func getIssueDetails(ctx context.Context, namespace, name string) (IssueDetails, error) {
if namespace == "" {
namespace = defaultNamespace
}
logs, err := pluginx.ExecuteCommand(ctx, fmt.Sprintf("kubectl logs %s -n %s --tail %d", name, namespace, logsTailLines))
if err != nil {
return IssueDetails{}, fmt.Errorf("while getting logs: %w", err)
}
ver, err := pluginx.ExecuteCommand(ctx, "kubectl version -o yaml")
if err != nil {
return IssueDetails{}, fmt.Errorf("while getting version: %w", err)
}

return IssueDetails{
Type: name,
Namespace: namespace,
Logs: logs,
Version: ver,
}, nil
}

const defaultIssueBody = `
## Description
This issue refers to the problems connected with {{ .Type | code "bash" }} in namespace {{ .Namespace | code "bash" }}
<details>
<summary><b>Logs</b></summary>
{{ .Logs | code "bash"}}
</details>
### Cluster details
{{ .Version | code "yaml"}}
`

func renderIssueBody(bodyTpl string, data IssueDetails) (string, error) {
if bodyTpl == "" {
bodyTpl = defaultIssueBody
}

tmpl, err := template.New("issue-body").Funcs(template.FuncMap{
"code": func(syntax, in string) string {
return fmt.Sprintf("\n```%s\n%s\n```\n", syntax, in)
},
}).Parse(bodyTpl)
if err != nil {
return "", fmt.Errorf("while creating template: %w", err)
}

var body bytes.Buffer
err = tmpl.Execute(&body, data)
if err != nil {
return "", fmt.Errorf("while generating body: %w", err)
}

return body.String(), nil
}

0 comments on commit 3ebc48e

Please sign in to comment.