From a8f6f2ec586a49e1e414d543e3314b1355024f22 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Wed, 21 Feb 2024 22:28:01 +0100 Subject: [PATCH] Refactor auth checker logic --- cmd/executor/ai-face/main.go | 6 +- cmd/executor/exec/main.go | 4 +- cmd/executor/gh/main.go | 4 +- cmd/executor/thread-mate/main.go | 1 + cmd/source/ai-brain/main.go | 6 +- go.mod | 2 +- internal/auth/executor.go | 73 ++++++++++++++++ internal/auth/msg.go | 39 +++++++++ internal/auth/source.go | 87 ++++++++++++++++++++ internal/executor/authorized/executor.go | 39 --------- internal/executor/doctor/executor.go | 11 ++- internal/executor/flux/executor.go | 9 +- internal/executor/helm/executor.go | 6 +- internal/executor/helm/executor_test.go | 70 +++++++++------- internal/remote/config.go | 8 +- internal/remote/conn_checker.go | 67 +++++++++++++++ internal/remote/deploy_client.go | 23 ++++-- internal/remote/gql_client.go | 54 ++---------- internal/source/argocd/argocd.go | 12 +-- internal/source/authorized/source.go | 39 --------- internal/source/github_events/source_grpc.go | 5 +- internal/source/keptn/source.go | 6 +- internal/source/prometheus/source.go | 6 +- 23 files changed, 376 insertions(+), 201 deletions(-) create mode 100644 internal/auth/executor.go create mode 100644 internal/auth/msg.go create mode 100644 internal/auth/source.go delete mode 100644 internal/executor/authorized/executor.go create mode 100644 internal/remote/conn_checker.go delete mode 100644 internal/source/authorized/source.go diff --git a/cmd/executor/ai-face/main.go b/cmd/executor/ai-face/main.go index dceb73c3..28f3056c 100644 --- a/cmd/executor/ai-face/main.go +++ b/cmd/executor/ai-face/main.go @@ -9,6 +9,8 @@ import ( "io" "net/http" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" + "github.com/hashicorp/go-plugin" aibrain "github.com/kubeshop/botkube-cloud-plugins/internal/source/ai-brain" "github.com/kubeshop/botkube/pkg/api" @@ -126,9 +128,9 @@ func (*AIFace) Help(context.Context) (api.Message, error) { func main() { executor.Serve(map[string]plugin.Plugin{ pluginName: &executor.Plugin{ - Executor: &AIFace{ + Executor: auth.NewProtectedExecutor(&AIFace{ httpClient: httpx.NewHTTPClient(), - }, + }), }, }) } diff --git a/cmd/executor/exec/main.go b/cmd/executor/exec/main.go index 95c95568..f2d04a81 100644 --- a/cmd/executor/exec/main.go +++ b/cmd/executor/exec/main.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" + "github.com/MakeNowJust/heredoc" "github.com/alexflint/go-arg" "github.com/hashicorp/go-plugin" @@ -202,7 +204,7 @@ func (i *XExecutor) getKubeconfig(ctx context.Context, log logrus.FieldLogger, i func main() { executor.Serve(map[string]plugin.Plugin{ pluginName: &executor.Plugin{ - Executor: &XExecutor{}, + Executor: auth.NewProtectedExecutor(&XExecutor{}), }, }) } diff --git a/cmd/executor/gh/main.go b/cmd/executor/gh/main.go index 61821b5b..06422f00 100644 --- a/cmd/executor/gh/main.go +++ b/cmd/executor/gh/main.go @@ -7,6 +7,8 @@ import ( "fmt" "text/template" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" + "github.com/hashicorp/go-plugin" "github.com/kubeshop/botkube/pkg/api" @@ -161,7 +163,7 @@ var depsDownloadLinks = map[string]api.Dependency{ func main() { executor.Serve(map[string]plugin.Plugin{ pluginName: &executor.Plugin{ - Executor: &GHExecutor{}, + Executor: auth.NewProtectedExecutor(&GHExecutor{}), }, }) } diff --git a/cmd/executor/thread-mate/main.go b/cmd/executor/thread-mate/main.go index dddc2b90..a083f55d 100644 --- a/cmd/executor/thread-mate/main.go +++ b/cmd/executor/thread-mate/main.go @@ -28,6 +28,7 @@ type ThreadMateExecutor struct { } func NewThreadMateExecutor() *ThreadMateExecutor { + // It's not protected as we use it only as dev plugin, and we integrate it with the SocketSlack. return &ThreadMateExecutor{} } diff --git a/cmd/source/ai-brain/main.go b/cmd/source/ai-brain/main.go index 16c32ad1..4feceecb 100644 --- a/cmd/source/ai-brain/main.go +++ b/cmd/source/ai-brain/main.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" + "github.com/hashicorp/go-plugin" aibrain "github.com/kubeshop/botkube-cloud-plugins/internal/source/ai-brain" "github.com/kubeshop/botkube/pkg/api" @@ -88,9 +90,9 @@ func (a *AI) HandleExternalRequest(_ context.Context, in source.ExternalRequestI func main() { source.Serve(map[string]plugin.Plugin{ pluginName: &source.Plugin{ - Source: &AI{ + Source: auth.NewProtectedSource(&AI{ incomingPrompts: sync.Map{}, - }, + }), }, }) } diff --git a/go.mod b/go.mod index aa0dc604..83eec8b4 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/hashicorp/go-getter v1.7.1 github.com/hashicorp/go-plugin v1.4.10 + github.com/hasura/go-graphql-client v0.8.1 github.com/huandu/xstrings v1.4.0 github.com/keptn/go-utils v0.20.4 github.com/kubeshop/botkube v0.13.1-0.20240219113106-577e1e800cff @@ -105,7 +106,6 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect - github.com/hasura/go-graphql-client v0.8.1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/internal/auth/executor.go b/internal/auth/executor.go new file mode 100644 index 00000000..eb30cd74 --- /dev/null +++ b/internal/auth/executor.go @@ -0,0 +1,73 @@ +package auth + +import ( + "context" + + "github.com/kubeshop/botkube-cloud-plugins/internal/remote" + + "github.com/kubeshop/botkube/pkg/api" + "github.com/kubeshop/botkube/pkg/api/executor" +) + +// ProtectedExecutor protects Executor usage without active Cloud connection. +type ProtectedExecutor struct { + underlying executor.Executor + openSourceBlockage bool + initConnectionBlockage bool + authChecker *remote.ConnChecker +} + +// NewProtectedExecutor returns wrapped Executor instance with Cloud connection checker. +func NewProtectedExecutor(exec executor.Executor) executor.Executor { + cfg, ok := remote.GetConfig() + if !ok { + return &ProtectedExecutor{ + underlying: exec, + openSourceBlockage: true, + } + } + + authChecker := remote.NewConnChecker(cfg) + err := authChecker.IsConnectedWithCould() + if err != nil { + return &ProtectedExecutor{ + underlying: exec, + initConnectionBlockage: true, + } + } + noop := func() {} + authChecker.AsyncSupervisor(noop) + + return &ProtectedExecutor{ + underlying: exec, + authChecker: authChecker, + } +} + +// Execute provides executor functionality only with active Cloud connection. +func (e *ProtectedExecutor) Execute(ctx context.Context, input executor.ExecuteInput) (executor.ExecuteOutput, error) { + if e.isBlocked() { + return executor.ExecuteOutput{ + Message: unauthorizedMessage(e.openSourceBlockage), + }, nil + } + + return e.underlying.Execute(ctx, input) +} + +// Metadata returns plugin metadata even without active Cloud connection. +func (e *ProtectedExecutor) Metadata(ctx context.Context) (api.MetadataOutput, error) { + return e.underlying.Metadata(ctx) +} + +// Help provides help functionality only with active Cloud connection. +func (e *ProtectedExecutor) Help(ctx context.Context) (api.Message, error) { + if e.isBlocked() { + return unauthorizedMessage(e.openSourceBlockage), nil + } + return e.underlying.Help(ctx) +} + +func (e *ProtectedExecutor) isBlocked() bool { + return e.openSourceBlockage || e.initConnectionBlockage || e.authChecker.ReachedPermanentBlockage() +} diff --git a/internal/auth/msg.go b/internal/auth/msg.go new file mode 100644 index 00000000..ab2b11fd --- /dev/null +++ b/internal/auth/msg.go @@ -0,0 +1,39 @@ +package auth + +import "github.com/kubeshop/botkube/pkg/api" + +func unauthorizedMessage(isOpenSourceInstance bool) api.Message { + btnBuilder := api.NewMessageButtonBuilder() + + if isOpenSourceInstance { + return api.Message{ + Sections: []api.Section{ + { + Base: api.Base{ + Header: "Cloud-Only Plugin Enabled", + Description: "This plugin is available only on Botkube Cloud. To use it, migrate your Botkube Agent instance to Botkube Cloud.", + }, + Buttons: []api.Button{ + btnBuilder.ForURL("Migrate Installation", "https://docs.botkube.io/cli/migrating-installation-to-botkube-cloud", api.ButtonStylePrimary), + btnBuilder.ForURL("Open Botkube Cloud", "https://app.botkube.io"), + }, + }, + }, + } + } + + return api.Message{ + Sections: []api.Section{ + { + Base: api.Base{ + Header: "Lost Connection with Botkube Cloud", + Description: "This plugin requires an active connection to Botkube Cloud. Ensure that your Agent has access to Botkube Cloud with valid credentials. Check Agent logs for more information.", + }, + Buttons: []api.Button{ + btnBuilder.ForURL("See Troubleshooting Guide", "https://docs.botkube.io/operation/common-problems"), + btnBuilder.ForURL("How to Get Agent Logs", "https://docs.botkube.io/operation/diagnostics#agent-logs"), + }, + }, + }, + } +} diff --git a/internal/auth/source.go b/internal/auth/source.go new file mode 100644 index 00000000..0be01fd8 --- /dev/null +++ b/internal/auth/source.go @@ -0,0 +1,87 @@ +package auth + +import ( + "context" + "log" + + "github.com/kubeshop/botkube-cloud-plugins/internal/remote" + + "github.com/kubeshop/botkube/pkg/api" + "github.com/kubeshop/botkube/pkg/api/source" +) + +// ProtectedSource protects source usage without active cloud connection. +type ProtectedSource struct { + underlying source.Source + openSourceBlockage bool + initConnectionBlockage bool +} + +// NewProtectedSource returns wrapped Source instance with Cloud conn checker. +func NewProtectedSource(source source.Source) source.Source { + cfg, ok := remote.GetConfig() + if !ok { + return &ProtectedSource{ + underlying: source, + openSourceBlockage: true, + } + } + authChecker := remote.NewConnChecker(cfg) + err := authChecker.IsConnectedWithCould() + if err != nil { + return &ProtectedSource{ + underlying: source, + initConnectionBlockage: true, + } + } + + onPermanentBlockage := func() { + // we may already start multiple streams (`Stream()`), + // so we want to kill the plugin process to stop all of them. + // Once restarted, and we will be still blocked, a proper message will be sent to all configured channels. + log.Fatal("Failed to connect with Cloud") + } + authChecker.AsyncSupervisor(onPermanentBlockage) + return &ProtectedSource{ + underlying: source, + } +} + +// Stream provides stream functionality only with active cloud connection. +func (s *ProtectedSource) Stream(ctx context.Context, in source.StreamInput) (source.StreamOutput, error) { + if s.isBlocked() { + out := source.StreamOutput{ + Event: make(chan source.Event, 1), + } + out.Event <- source.Event{ + Message: unauthorizedMessage(s.openSourceBlockage), + } + + return out, nil + } + + return s.underlying.Stream(ctx, in) +} + +// Metadata returns plugin metadata even without active cloud connection. +func (s *ProtectedSource) Metadata(ctx context.Context) (api.MetadataOutput, error) { + return s.underlying.Metadata(ctx) +} + +// HandleExternalRequest provides external request functionality only with active cloud connection. +func (s *ProtectedSource) HandleExternalRequest(ctx context.Context, in source.ExternalRequestInput) (source.ExternalRequestOutput, error) { + if s.isBlocked() { + out := source.ExternalRequestOutput{ + Event: source.Event{ + Message: unauthorizedMessage(s.openSourceBlockage), + }, + } + return out, nil + } + + return s.underlying.HandleExternalRequest(ctx, in) +} + +func (s *ProtectedSource) isBlocked() bool { + return s.openSourceBlockage || s.initConnectionBlockage +} diff --git a/internal/executor/authorized/executor.go b/internal/executor/authorized/executor.go deleted file mode 100644 index 6c7e53f6..00000000 --- a/internal/executor/authorized/executor.go +++ /dev/null @@ -1,39 +0,0 @@ -package authorized - -import ( - "context" - - "github.com/kubeshop/botkube-cloud-plugins/internal/remote" - - "github.com/kubeshop/botkube/pkg/api" - "github.com/kubeshop/botkube/pkg/api/executor" -) - -type unauthorizedExecutor struct { - metadata api.MetadataOutput -} - -func NewExecutor(exec executor.Executor) executor.Executor { - deployClient := remote.NewDeploymentClient(remote.NewDefaultGqlClient()) - ok := deployClient.IsConnectedWithCould() - if !ok { - metadata, _ := exec.Metadata(context.Background()) - return &unauthorizedExecutor{ - metadata: metadata, - } - } - - return exec -} - -func (e unauthorizedExecutor) Execute(context.Context, executor.ExecuteInput) (executor.ExecuteOutput, error) { - panic("couldn't run the plugin: invalid license") -} - -func (e unauthorizedExecutor) Metadata(context.Context) (api.MetadataOutput, error) { - return e.metadata, nil -} - -func (e unauthorizedExecutor) Help(context.Context) (api.Message, error) { - panic("couldn't run the plugin: invalid license") -} diff --git a/internal/executor/doctor/executor.go b/internal/executor/doctor/executor.go index a386bab0..405ac7ac 100644 --- a/internal/executor/doctor/executor.go +++ b/internal/executor/doctor/executor.go @@ -9,17 +9,16 @@ import ( "strings" "sync" - "github.com/PullRequestInc/go-gpt3" - "github.com/sirupsen/logrus" - stringsutil "k8s.io/utils/strings" - - "github.com/kubeshop/botkube-cloud-plugins/internal/executor/authorized" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" + "github.com/PullRequestInc/go-gpt3" "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/executor" "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/loggerx" pluginx "github.com/kubeshop/botkube/pkg/plugin" + "github.com/sirupsen/logrus" + stringsutil "k8s.io/utils/strings" ) const ( @@ -59,7 +58,7 @@ func NewExecutor(ver string) executor.Executor { exec := &Executor{ pluginVersion: ver, } - return authorized.NewExecutor(exec) + return auth.NewProtectedExecutor(exec) } // Metadata returns details about the Doctor plugin. diff --git a/internal/executor/flux/executor.go b/internal/executor/flux/executor.go index fb405b06..89064e7b 100644 --- a/internal/executor/flux/executor.go +++ b/internal/executor/flux/executor.go @@ -5,14 +5,13 @@ import ( _ "embed" "fmt" - "github.com/allegro/bigcache/v3" - - "github.com/kubeshop/botkube-cloud-plugins/internal/executor/authorized" - + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" "github.com/kubeshop/botkube-cloud-plugins/internal/executor/flux/commands" "github.com/kubeshop/botkube-cloud-plugins/internal/executor/x" "github.com/kubeshop/botkube-cloud-plugins/internal/executor/x/output" "github.com/kubeshop/botkube-cloud-plugins/internal/executor/x/state" + + "github.com/allegro/bigcache/v3" "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/executor" "github.com/kubeshop/botkube/pkg/loggerx" @@ -43,7 +42,7 @@ func NewExecutor(cache *bigcache.BigCache, ver string) executor.Executor { pluginVersion: ver, cache: cache, } - return authorized.NewExecutor(exec) + return auth.NewProtectedExecutor(exec) } // Metadata returns details about the Flux plugin. diff --git a/internal/executor/helm/executor.go b/internal/executor/helm/executor.go index 6fa80491..2d230b3f 100644 --- a/internal/executor/helm/executor.go +++ b/internal/executor/helm/executor.go @@ -4,11 +4,11 @@ import ( "context" _ "embed" "fmt" - "github.com/kubeshop/botkube-cloud-plugins/internal/executor/authorized" "os" - "github.com/alexflint/go-arg" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" + "github.com/alexflint/go-arg" "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/executor" pluginx "github.com/kubeshop/botkube/pkg/plugin" @@ -61,7 +61,7 @@ func NewExecutor(ver string) executor.Executor { pluginVersion: ver, executeCommand: pluginx.ExecuteCommand, } - return authorized.NewExecutor(exec) + return auth.NewProtectedExecutor(exec) } // Metadata returns details about Helm plugin. diff --git a/internal/executor/helm/executor_test.go b/internal/executor/helm/executor_test.go index 8a3a0a09..0cac6e1e 100644 --- a/internal/executor/helm/executor_test.go +++ b/internal/executor/helm/executor_test.go @@ -48,25 +48,27 @@ func TestExecutorHelmInstall(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // given - execOutput := pluginx.ExecuteCommandOutput{ - Stdout: "mocked", - } - - hExec := NewExecutor("testing") - var ( gotCmd string gotEnvs map[string]string ) - hExec.executeCommand = func(ctx context.Context, rawCmd string, mutators ...pluginx.ExecuteCommandMutation) (pluginx.ExecuteCommandOutput, error) { - gotCmd = rawCmd - var opts pluginx.ExecuteCommandOptions - for _, mutate := range mutators { - mutate(&opts) - } - - gotEnvs = opts.Envs - return execOutput, nil + + execOutput := pluginx.ExecuteCommandOutput{ + Stdout: "mocked", + } + + hExec := &Executor{ + pluginVersion: "testing", + executeCommand: func(ctx context.Context, rawCmd string, mutators ...pluginx.ExecuteCommandMutation) (pluginx.ExecuteCommandOutput, error) { + gotCmd = rawCmd + var opts pluginx.ExecuteCommandOptions + for _, mutate := range mutators { + mutate(&opts) + } + + gotEnvs = opts.Envs + return execOutput, nil + }, } // when @@ -121,8 +123,10 @@ func TestExecutorHelmInstallFlagsErrors(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // given - hExec := NewExecutor("testing") - hExec.executeCommand = noopExecuteCommand + hExec := &Executor{ + pluginVersion: "testing", + executeCommand: noopExecuteCommand, + } // when out, err := hExec.Execute(context.Background(), executor.ExecuteInput{ @@ -161,8 +165,10 @@ func TestExecutorHelmInstallHelp(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // given - hExec := NewExecutor("testing") - hExec.executeCommand = noopExecuteCommand + hExec := &Executor{ + pluginVersion: "testing", + executeCommand: noopExecuteCommand, + } // when out, err := hExec.Execute(context.Background(), executor.ExecuteInput{ @@ -184,16 +190,18 @@ func TestExecutorHelmInstallHelp(t *testing.T) { func TestExecutorConfigMerging(t *testing.T) { // given - hExec := NewExecutor("testing") var gotEnvs map[string]string - hExec.executeCommand = func(ctx context.Context, rawCmd string, mutators ...pluginx.ExecuteCommandMutation) (pluginx.ExecuteCommandOutput, error) { - var opts pluginx.ExecuteCommandOptions - for _, mutate := range mutators { - mutate(&opts) - } - - gotEnvs = opts.Envs - return pluginx.ExecuteCommandOutput{}, nil + hExec := &Executor{ + pluginVersion: "testing", + executeCommand: func(ctx context.Context, rawCmd string, mutators ...pluginx.ExecuteCommandMutation) (pluginx.ExecuteCommandOutput, error) { + var opts pluginx.ExecuteCommandOptions + for _, mutate := range mutators { + mutate(&opts) + } + + gotEnvs = opts.Envs + return pluginx.ExecuteCommandOutput{}, nil + }, } configA := Config{ @@ -234,8 +242,10 @@ func TestExecutorConfigMerging(t *testing.T) { func TestExecutorConfigMergingErrors(t *testing.T) { // given - hExec := NewExecutor("testing") - hExec.executeCommand = noopExecuteCommand + hExec := &Executor{ + pluginVersion: "testing", + executeCommand: noopExecuteCommand, + } configA := Config{ HelmDriver: "unknown-value", diff --git a/internal/remote/config.go b/internal/remote/config.go index 28619eec..046b005f 100644 --- a/internal/remote/config.go +++ b/internal/remote/config.go @@ -23,10 +23,14 @@ type Config struct { } // GetConfig returns remote configuration if it is set. -func GetConfig() Config { +func GetConfig() (Config, bool) { + if os.Getenv(ProviderIdentifierEnvKey) == "" { + return Config{}, false + } + return Config{ Endpoint: os.Getenv(ProviderEndpointEnvKey), Identifier: os.Getenv(ProviderIdentifierEnvKey), APIKey: os.Getenv(ProviderAPIKeyEnvKey), - } + }, true } diff --git a/internal/remote/conn_checker.go b/internal/remote/conn_checker.go new file mode 100644 index 00000000..04ddf954 --- /dev/null +++ b/internal/remote/conn_checker.go @@ -0,0 +1,67 @@ +package remote + +import ( + "sync/atomic" + "time" + + "github.com/kubeshop/botkube/pkg/config" + "github.com/kubeshop/botkube/pkg/loggerx" + "github.com/sirupsen/logrus" +) + +const ( + connectionRecheckDuration = time.Minute + maxFailedAttempts = 3 +) + +// ConnChecker checks if Cloud connection is active. +type ConnChecker struct { + failedAttempts atomic.Uint32 + deployClient *DeploymentClient + log *logrus.Entry +} + +// NewConnChecker returns new connection checker. +func NewConnChecker(cfg Config) *ConnChecker { + return &ConnChecker{ + deployClient: NewDeploymentClient(cfg), + log: loggerx.New(config.Logger{ + Level: "info", + }).WithField("service", "auth-checker"), + } +} + +// IsConnectedWithCould returns error if Cloud connection is not active. +func (a *ConnChecker) IsConnectedWithCould() error { + return a.deployClient.IsConnectedWithCould() +} + +// AsyncSupervisor starts connection checker. +func (a *ConnChecker) AsyncSupervisor(onPermanentBlockage func()) { + go func() { + timer := time.NewTimer(connectionRecheckDuration) + defer timer.Stop() + + for { + timer.Reset(connectionRecheckDuration) + <-timer.C + err := a.deployClient.IsConnectedWithCould() + if err != nil { + a.log.WithError(err).Error("Failed to connect with Cloud") + + a.failedAttempts.Add(1) + if a.ReachedPermanentBlockage() { + onPermanentBlockage() + return + } + continue + } + a.failedAttempts.Store(0) + } + }() +} + +// ReachedPermanentBlockage returns true if reached max failed attempts. +func (a *ConnChecker) ReachedPermanentBlockage() bool { + return a.failedAttempts.Load() > maxFailedAttempts +} diff --git a/internal/remote/deploy_client.go b/internal/remote/deploy_client.go index ad1c7099..44724bd1 100644 --- a/internal/remote/deploy_client.go +++ b/internal/remote/deploy_client.go @@ -2,6 +2,7 @@ package remote import ( "context" + "fmt" "time" "github.com/avast/retry-go/v4" @@ -20,8 +21,8 @@ type DeploymentClient struct { } // NewDeploymentClient initializes GraphQL client. -func NewDeploymentClient(client GraphQLClient) *DeploymentClient { - return &DeploymentClient{client: client} +func NewDeploymentClient(cfg Config) *DeploymentClient { + return &DeploymentClient{client: NewDefaultGqlClient(cfg)} } // Deployment returns deployment with Botkube configuration. @@ -30,7 +31,7 @@ type Deployment struct { } // IsConnectedWithCould returns whether connected to Botkube Cloud -func (d *DeploymentClient) IsConnectedWithCould() bool { +func (d *DeploymentClient) IsConnectedWithCould() error { var query struct { Deployment struct { ID string @@ -40,17 +41,22 @@ func (d *DeploymentClient) IsConnectedWithCould() bool { variables := map[string]interface{}{ "id": graphql.ID(deployID), } - err := d.withRetries(context.Background(), 5, func() error { + err := d.withRetries(5, func() error { return d.client.Client().Query(context.Background(), &query, variables) }) + if err != nil { - return false + return err + } + + if query.Deployment.ID != deployID { + return fmt.Errorf("instance with id %q is not recognized by Cloud", deployID) } - return query.Deployment.ID == d.client.DeploymentID() + return nil } -func (d *DeploymentClient) withRetries(ctx context.Context, maxRetries int, fn func() error) error { +func (d *DeploymentClient) withRetries(maxRetries int, fn func() error) error { return retry.Do( func() error { return fn() @@ -58,7 +64,6 @@ func (d *DeploymentClient) withRetries(ctx context.Context, maxRetries int, fn f retry.DelayType(func(uint, error, *retry.Config) time.Duration { return 200 * time.Microsecond }), - retry.Attempts(uint(maxRetries)), // infinite, we cancel that by our own - retry.Context(ctx), + retry.Attempts(uint(maxRetries)), ) } diff --git a/internal/remote/gql_client.go b/internal/remote/gql_client.go index 23926b25..de5590d9 100644 --- a/internal/remote/gql_client.go +++ b/internal/remote/gql_client.go @@ -13,63 +13,23 @@ const ( apiKeyHeaderName = "X-API-Key" ) -// Option define GraphQL client option. -type Option func(*Gql) - -// WithEndpoint configures ApiURL for GraphQL endpoint. -func WithEndpoint(url string) Option { - return func(client *Gql) { - client.endpoint = url - } -} - -// WithAPIKey configures API key for GraphQL endpoint. -func WithAPIKey(apiKey string) Option { - return func(client *Gql) { - client.apiKey = apiKey - } -} - -// WithDeploymentID configures deployment id for GraphQL endpoint. -func WithDeploymentID(id string) Option { - return func(client *Gql) { - client.deployID = id - } -} - // Gql defines GraphQL client data structure. type Gql struct { client *graphql.Client - endpoint string - apiKey string deployID string } -// NewGqlClient initializes GraphQL client. -func NewGqlClient(options ...Option) *Gql { - c := &Gql{} - for _, opt := range options { - opt(c) - } - +// NewDefaultGqlClient initializes GraphQL client with default options. +func NewDefaultGqlClient(remoteCfg Config) *Gql { httpCli := &http.Client{ - Transport: newAPIKeySecuredTransport(c.apiKey), + Transport: newAPIKeySecuredTransport(remoteCfg.APIKey), Timeout: defaultTimeout, } - c.client = graphql.NewClient(c.endpoint, httpCli) - return c -} - -// NewDefaultGqlClient initializes GraphQL client with default options. -func NewDefaultGqlClient() *Gql { - remoteCfg := GetConfig() - - return NewGqlClient( - WithEndpoint(remoteCfg.Endpoint), - WithAPIKey(remoteCfg.APIKey), - WithDeploymentID(remoteCfg.Identifier), - ) + return &Gql{ + client: graphql.NewClient(remoteCfg.Endpoint, httpCli), + deployID: remoteCfg.Identifier, + } } // DeploymentID returns deployment ID. diff --git a/internal/source/argocd/argocd.go b/internal/source/argocd/argocd.go index e469575d..19f99515 100644 --- a/internal/source/argocd/argocd.go +++ b/internal/source/argocd/argocd.go @@ -8,18 +8,18 @@ import ( "sync" "time" - "github.com/avast/retry-go/v4" - "github.com/sirupsen/logrus" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/tools/clientcmd" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" - "github.com/kubeshop/botkube-cloud-plugins/internal/source/authorized" + "github.com/avast/retry-go/v4" "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/source" "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/formatx" "github.com/kubeshop/botkube/pkg/loggerx" pluginx "github.com/kubeshop/botkube/pkg/plugin" + "github.com/sirupsen/logrus" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/tools/clientcmd" ) var _ source.Source = (*Source)(nil) @@ -62,7 +62,7 @@ func NewSource(version string) source.Source { pluginVersion: version, cfgs: sync.Map{}, } - return authorized.NewSource(src) + return auth.NewProtectedSource(src) } type subscription struct { diff --git a/internal/source/authorized/source.go b/internal/source/authorized/source.go deleted file mode 100644 index c65486b6..00000000 --- a/internal/source/authorized/source.go +++ /dev/null @@ -1,39 +0,0 @@ -package authorized - -import ( - "context" - - "github.com/kubeshop/botkube-cloud-plugins/internal/remote" - - "github.com/kubeshop/botkube/pkg/api" - "github.com/kubeshop/botkube/pkg/api/source" -) - -type unauthorizedSource struct { - metadata api.MetadataOutput -} - -func NewSource(source source.Source) source.Source { - deployClient := remote.NewDeploymentClient(remote.NewDefaultGqlClient()) - ok := deployClient.IsConnectedWithCould() - if !ok { - metadata, _ := source.Metadata(context.Background()) - return &unauthorizedSource{ - metadata: metadata, - } - } - - return source -} - -func (s unauthorizedSource) Stream(context.Context, source.StreamInput) (source.StreamOutput, error) { - panic("couldn't run the plugin: invalid license") -} - -func (s unauthorizedSource) Metadata(context.Context) (api.MetadataOutput, error) { - return s.metadata, nil -} - -func (s unauthorizedSource) HandleExternalRequest(context.Context, source.ExternalRequestInput) (source.ExternalRequestOutput, error) { - panic("couldn't run the plugin: invalid license") -} diff --git a/internal/source/github_events/source_grpc.go b/internal/source/github_events/source_grpc.go index e3af0387..c04bbe84 100644 --- a/internal/source/github_events/source_grpc.go +++ b/internal/source/github_events/source_grpc.go @@ -5,8 +5,9 @@ import ( _ "embed" "fmt" - "github.com/kubeshop/botkube-cloud-plugins/internal/source/authorized" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" "github.com/kubeshop/botkube-cloud-plugins/internal/source/github_events/gh" + "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/source" "github.com/kubeshop/botkube/pkg/loggerx" @@ -38,7 +39,7 @@ func NewSource(version string) source.Source { src := &Source{ pluginVersion: version, } - return authorized.NewSource(src) + return auth.NewProtectedSource(src) } // Stream streams GitHub events. diff --git a/internal/source/keptn/source.go b/internal/source/keptn/source.go index 5e7c72ab..cdb273be 100644 --- a/internal/source/keptn/source.go +++ b/internal/source/keptn/source.go @@ -6,12 +6,12 @@ import ( "fmt" "time" - "github.com/sirupsen/logrus" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" - "github.com/kubeshop/botkube-cloud-plugins/internal/source/authorized" "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/source" "github.com/kubeshop/botkube/pkg/loggerx" + "github.com/sirupsen/logrus" ) var _ source.Source = (*Source)(nil) @@ -42,7 +42,7 @@ func NewSource(version string) source.Source { src := &Source{ pluginVersion: version, } - return authorized.NewSource(src) + return auth.NewProtectedSource(src) } // Stream streams Keptn events diff --git a/internal/source/prometheus/source.go b/internal/source/prometheus/source.go index 32b3942a..d258492b 100644 --- a/internal/source/prometheus/source.go +++ b/internal/source/prometheus/source.go @@ -6,12 +6,12 @@ import ( "fmt" "time" - "github.com/sirupsen/logrus" + "github.com/kubeshop/botkube-cloud-plugins/internal/auth" - "github.com/kubeshop/botkube-cloud-plugins/internal/source/authorized" "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/api/source" "github.com/kubeshop/botkube/pkg/loggerx" + "github.com/sirupsen/logrus" ) var _ source.Source = (*Source)(nil) @@ -44,7 +44,7 @@ func NewSource(version string) source.Source { pluginVersion: version, startedAt: time.Now(), } - return authorized.NewSource(src) + return auth.NewProtectedSource(src) } // Stream streams prometheus alerts