-
Notifications
You must be signed in to change notification settings - Fork 291
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
28 changed files
with
1,998 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
Oops, something went wrong.