Skip to content

Commit

Permalink
Add thread mate plugin (#1294)
Browse files Browse the repository at this point in the history
  • Loading branch information
mszostok authored Oct 16, 2023
1 parent a16055c commit 69e606a
Show file tree
Hide file tree
Showing 28 changed files with 1,998 additions and 242 deletions.
21 changes: 21 additions & 0 deletions .goreleaser.plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ builds:
main: cmd/executor/kubectl/main.go
binary: executor_kubectl_{{ .Os }}_{{ .Arch }}

no_unique_dist_dir: true
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
goarm:
- 7
- id: thread-mate
main: cmd/executor/thread-mate/main.go
binary: executor_thread-mate_{{ .Os }}_{{ .Arch }}

no_unique_dist_dir: true
env:
- CGO_ENABLED=0
Expand Down Expand Up @@ -249,6 +264,12 @@ archives:
- none*
name_template: "{{ .Binary }}"

- builds: [thread-mate]
id: thread-mate
files:
- none*
name_template: "{{ .Binary }}"

- builds: [argocd]
id: argocd
files:
Expand Down
159 changes: 159 additions & 0 deletions cmd/executor/thread-mate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package main

import (
"context"
"errors"
"fmt"
"sync"

"github.com/alexflint/go-arg"
"github.com/hashicorp/go-plugin"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"

thmate "github.com/kubeshop/botkube/internal/executor/thread-mate"
"github.com/kubeshop/botkube/pkg/api"
"github.com/kubeshop/botkube/pkg/api/executor"
"github.com/kubeshop/botkube/pkg/pluginx"
)

const pluginName = "thread-mate"

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

// ThreadMateExecutor implements the Botkube executor plugin interface.
type ThreadMateExecutor struct {
once sync.Map
}

func NewThreadMateExecutor() *ThreadMateExecutor {
return &ThreadMateExecutor{}
}

// Metadata returns details about plugin.
func (*ThreadMateExecutor) Metadata(context.Context) (api.MetadataOutput, error) {
return api.MetadataOutput{
Version: version,
Description: "Streamlines managing assignment for incidents or user support",
JSONSchema: api.JSONSchema{
Value: thmate.JSONSchema,
},
}, nil
}

func (t *ThreadMateExecutor) init(cfg thmate.Config, kubeconfig []byte) (*thmate.ThreadMate, error) {
svc, ok := t.once.Load(cfg.RoundRobin.GroupName)
if ok {
return svc.(*thmate.ThreadMate), nil
}
kubeConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig)
if err != nil {
return nil, fmt.Errorf("while reading kube config. %w", err)
}
k8sCli, err := kubernetes.NewForConfig(kubeConfig)
if err != nil {
return nil, fmt.Errorf("while creating K8s clientset: %w", err)
}

cfgDumper := thmate.NewConfigMapDumper(k8sCli)

newSvc := thmate.New(cfg, cfgDumper)
newSvc.Start()

t.once.Store(cfg.RoundRobin.GroupName, newSvc)
return newSvc, nil
}

// Execute returns a given command as a response.
func (t *ThreadMateExecutor) Execute(ctx context.Context, in executor.ExecuteInput) (executor.ExecuteOutput, error) {
if err := pluginx.ValidateKubeConfigProvided(pluginName, in.Context.KubeConfig); err != nil {
return executor.ExecuteOutput{}, err
}

var cmd thmate.Commands
err := pluginx.ParseCommand(pluginName, in.Command, &cmd)
switch {
case err == nil:
case errors.Is(err, arg.ErrHelp):
msg, _ := t.Help(ctx)
return executor.ExecuteOutput{
Message: msg,
}, nil
default:
return executor.ExecuteOutput{}, fmt.Errorf("while parsing input command: %w", err)
}

cfg, err := thmate.MergeConfigs(in.Configs)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while merging configuration: %w", err)
}

svc, err := t.init(cfg, in.Context.KubeConfig)
if err != nil {
return executor.ExecuteOutput{}, fmt.Errorf("while initializing service: %w", err)
}

switch {
case cmd.Pick != nil:
msgs, err := svc.Pick(cmd.Pick, in.Context.Message)
if err != nil {
return executor.ExecuteOutput{}, err
}
if len(msgs) == 0 {
return executor.ExecuteOutput{
Message: api.Message{Type: api.SkipMessage},
}, nil
}
return executor.ExecuteOutput{
Messages: msgs,
}, nil
case cmd.Get != nil && cmd.Get.Activity != nil:
return executor.ExecuteOutput{
Message: svc.GetActivity(cmd.Get.Activity, in.Context.Message),
}, nil
case cmd.Resolve != nil:
return executor.ExecuteOutput{
Message: svc.Resolve(cmd.Resolve, in.Context.Message),
}, nil
case cmd.Takeover != nil:
return executor.ExecuteOutput{
Message: svc.Takeover(cmd.Takeover, in.Context.Message),
}, nil
case cmd.Export != nil:
return executor.ExecuteOutput{
Message: svc.Export(cmd.Export),
}, nil
default:
msg, _ := t.Help(ctx)
msg.BaseBody.Plaintext = "Please specify a valid command"
return executor.ExecuteOutput{
Message: msg,
}, nil
}
}

func (*ThreadMateExecutor) Help(context.Context) (api.Message, error) {
btnBuilder := api.NewMessageButtonBuilder()
return api.Message{
Sections: []api.Section{
{
Base: api.Base{
Header: "Streamlines managing assignment for incidents or user support",
},
Buttons: []api.Button{
btnBuilder.ForCommandWithDescCmd("Pick a person", "thread-mate pick"),
btnBuilder.ForCommandWithDescCmd("Get Activity", "thread-mate get activity"),
},
},
},
}, nil
}

func main() {
executor.Serve(map[string]plugin.Plugin{
pluginName: &executor.Plugin{
Executor: NewThreadMateExecutor(),
},
})
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/go-playground/validator/v10 v10.11.0
github.com/go-rod/rod v0.113.3
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
github.com/google/go-github/v53 v53.2.0
github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.3.0
Expand All @@ -49,6 +50,7 @@ require (
github.com/mattn/go-shellwords v1.0.12
github.com/morikuni/aec v1.0.0
github.com/muesli/reflow v0.3.0
github.com/olekukonko/tablewriter v0.0.5
github.com/olivere/elastic/v7 v7.0.32
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/pkg/errors v0.9.1
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
Expand Down Expand Up @@ -1018,6 +1020,7 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
Expand Down
83 changes: 83 additions & 0 deletions internal/executor/thread-mate/cm_sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package thread_mate

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

// dataFieldName is the key used to store data in the ConfigMap.
const dataFieldName = "data"

// ConfigMapDumper is a utility for working with Kubernetes ConfigMaps.
type ConfigMapDumper struct {
k8sCli kubernetes.Interface
}

// NewConfigMapDumper creates a new instance of ConfigMapDumper.
func NewConfigMapDumper(k8sCli kubernetes.Interface) *ConfigMapDumper {
return &ConfigMapDumper{
k8sCli: k8sCli,
}
}

// SaveOrUpdate saves or updates data in a ConfigMap in the specified namespace.
func (a *ConfigMapDumper) SaveOrUpdate(namespace, name, data string) error {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string]string{
dataFieldName: data,
},
}

ctx := context.Background()
_, err := a.k8sCli.CoreV1().ConfigMaps(namespace).Create(ctx, cm, metav1.CreateOptions{})
switch {
case err == nil:
case apierrors.IsAlreadyExists(err):
old, err := a.k8sCli.CoreV1().ConfigMaps(cm.Namespace).Get(ctx, cm.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("while getting already existing ConfigMap: %w", err)
}

newCM := old.DeepCopy()
if newCM.Data == nil {
newCM.Data = map[string]string{}
}
newCM.Data[dataFieldName] = data

_, err = a.k8sCli.CoreV1().ConfigMaps(cm.Namespace).Update(ctx, newCM, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("while updating ConfigMap: %w", err)
}

default:
return fmt.Errorf("while creating ConfigMap: %w", err)
}
return nil
}

// Get retrieves data from a ConfigMap in the specified namespace.
func (a *ConfigMapDumper) Get(namespace, name string) (string, error) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}

ctx := context.Background()
cm, err := a.k8sCli.CoreV1().ConfigMaps(cm.Namespace).Get(ctx, cm.Name, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("while getting ConfigMap: %w", err)
}

return cm.Data[dataFieldName], nil
}
65 changes: 65 additions & 0 deletions internal/executor/thread-mate/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package thread_mate

import "strings"

type (
// Commands represents a collection of subcommands.
Commands struct {
Pick *PickCmd `arg:"subcommand:pick"`
Get *GetCmd `arg:"subcommand:get"`
Resolve *ResolveCmd `arg:"subcommand:resolve"`
Takeover *TakeoverCmd `arg:"subcommand:takeover"`
Export *ExportCmd `arg:"subcommand:export"`
}

// ExportCmd represents the "export" subcommand.
ExportCmd struct {
Activity *ExportActivityCmd `arg:"subcommand:activity"`
}

// ExportActivityCmd represents the options for the "export activity" subcommand.
ExportActivityCmd struct {
Type string `arg:"--type"`
}

// ResolveCmd represents the "resolve" subcommand.
ResolveCmd struct {
ID string `arg:"--id"`
}

// TakeoverCmd represents the "takeover" subcommand.
TakeoverCmd struct {
ID string `arg:"--id"`
}

// PickCmd represents the "pick" subcommand.
PickCmd struct {
MessageContext string `arg:"-m,--message"`
}

// GetCmd represents the "get" subcommand.
GetCmd struct {
Activity *ActivityCmd `arg:"subcommand:activity"`
}

// ActivityCmd represents the "activity" subcommand under the "get" command.
ActivityCmd struct {
AssigneeIDs string `arg:"--assignee-ids"`
Type ThreadType `arg:"--thread-type"`
PageIdx int `arg:"-p,--page"`
}
)

type ThreadType string

const (
ThreadTypeOngoing = "ongoing"
ThreadTypeResolved = "resolved"
)

func (t ThreadType) IsEmptyOrEqual(exp ThreadType) bool {
if t == "" {
return true
}
return strings.EqualFold(string(t), string(exp))
}
Loading

0 comments on commit 69e606a

Please sign in to comment.