Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom headers during plugin index request #1381

Merged
merged 6 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ require (
github.com/gookit/color v1.5.2
github.com/gorilla/mux v1.8.0
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/hashicorp/go-getter v1.7.1
github.com/hashicorp/go-getter v1.7.3
github.com/hashicorp/go-hclog v1.5.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-plugin v1.4.10
Expand Down Expand Up @@ -280,3 +280,5 @@ require (
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

replace github.com/hashicorp/go-getter v1.7.3 => github.com/kubeshop/go-getter v0.0.0-20240219121353-061e752b1b28
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -694,8 +694,6 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY=
github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
Expand Down Expand Up @@ -819,6 +817,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubeshop/go-getter v0.0.0-20240219121353-061e752b1b28 h1:cIyWkSZbkn/7VU7/T375t9n7esO8/bruaJ82FIk4Gs4=
github.com/kubeshop/go-getter v0.0.0-20240219121353-061e752b1b28/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
Expand Down
6 changes: 4 additions & 2 deletions internal/cli/migrate/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ func (c *Converter) convertExecutors(executors map[string]bkconfig.Executors) ([

rawCfg, err := json.Marshal(p.Config)
if err != nil {
return nil, err
errs = multierror.Append(errs, fmt.Errorf("while marshalling config for executor %q: %w", name, err))
continue
}
displayName := conf.DisplayName
if displayName == "" {
Expand Down Expand Up @@ -148,7 +149,8 @@ func (c *Converter) convertSources(sources map[string]bkconfig.Sources) ([]*gqlM
}
rawCfg, err := json.Marshal(p.Config)
if err != nil {
return nil, err
errs = multierror.Append(errs, fmt.Errorf("while marshalling config for source %q: %w", name, err))
continue
}
displayName := conf.DisplayName
if displayName == "" {
Expand Down
17 changes: 9 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ type Config struct {

// PluginManagement holds Botkube plugin management related configuration.
type PluginManagement struct {
CacheDir string `yaml:"cacheDir"`
Repositories map[string]PluginsRepositories `yaml:"repositories"`
IncomingWebhook IncomingWebhook `yaml:"incomingWebhook"`
RestartPolicy PluginRestartPolicy `yaml:"restartPolicy"`
HealthCheckInterval time.Duration `yaml:"healthCheckInterval"`
CacheDir string `yaml:"cacheDir"`
Repositories map[string]PluginsRepository `yaml:"repositories"`
IncomingWebhook IncomingWebhook `yaml:"incomingWebhook"`
RestartPolicy PluginRestartPolicy `yaml:"restartPolicy"`
HealthCheckInterval time.Duration `yaml:"healthCheckInterval"`
}

type PluginRestartPolicy struct {
Expand All @@ -166,9 +166,10 @@ func (p PluginRestartPolicyType) ToLower() string {
return strings.ToLower(string(p))
}

// PluginsRepositories holds the Plugin repository information.
type PluginsRepositories struct {
URL string `yaml:"url"`
// PluginsRepository holds the Plugin repository information.
type PluginsRepository struct {
URL string `yaml:"url"`
Headers map[string]string
pkosiec marked this conversation as resolved.
Show resolved Hide resolved
}

// IncomingWebhook contains configuration for incoming source webhook.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ plugins:
repositories:
botkube:
url: http://localhost:3000/botkube.yaml
headers: {}
incomingWebhook:
enabled: false
port: 0
Expand Down
21 changes: 15 additions & 6 deletions pkg/plugin/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plugin
import (
"context"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
Expand All @@ -16,7 +17,7 @@ var allowedExt = map[string]struct{}{
}

// downloadBinary downloads binary into specific destination.
func downloadBinary(ctx context.Context, destPath string, url URL, autoDetectFilename bool) error {
func downloadBinary(ctx context.Context, destPath string, binaryURL URL, autoDetectFilename bool) error {
dir, filename := filepath.Split(destPath)
err := os.MkdirAll(dir, dirPerms)
if err != nil {
Expand All @@ -35,10 +36,18 @@ func downloadBinary(ctx context.Context, destPath string, url URL, autoDetectFil
}
}

urlWithGoGetterMagicParams := fmt.Sprintf("%s?filename=%s", url.URL, filename)
if url.Checksum != "" {
urlWithGoGetterMagicParams = fmt.Sprintf("%s&checksum=%s", urlWithGoGetterMagicParams, url.Checksum)
parsedURL, err := url.Parse(binaryURL.URL)
if err != nil {
return fmt.Errorf("while parsing URL %q: %w", binaryURL.URL, err)
}
// Add go-getter magic params
queryParams := parsedURL.Query()
queryParams.Set("filename", filename)
if binaryURL.Checksum != "" {
queryParams.Set("checksum", binaryURL.Checksum)
}
parsedURL.RawQuery = queryParams.Encode()
urlWithGoGetterMagicParams := parsedURL.String()

getterCli := &getter.Client{
Ctx: ctx,
Expand All @@ -50,11 +59,11 @@ func downloadBinary(ctx context.Context, destPath string, url URL, autoDetectFil

err = getterCli.Get()
if err != nil {
return fmt.Errorf("while downloading binary from URL %q: %w", url, err)
return fmt.Errorf("while downloading binary with go-getter via url %s: %w", urlWithGoGetterMagicParams, err)
}

if stat, err := os.Stat(tmpDestPath); err == nil && stat.IsDir() {
if autoDetectFilename && hasArchiveExtension(url.URL) {
if autoDetectFilename && hasArchiveExtension(parsedURL.Path) {
filename, err = getFirstFileInDirectory(tmpDestPath)
if err != nil {
return fmt.Errorf("while getting binary name")
Expand Down
32 changes: 32 additions & 0 deletions pkg/plugin/index.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package plugin

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"

"github.com/dustin/go-humanize"
Expand Down Expand Up @@ -154,3 +157,32 @@ func (in Index) Validate() error {

return issues.ErrorOrNil()
}

// Get returns the JSON schema.
// When RefURL is not empty, Get tries to fetch the schema from the URL.
func (s *JSONSchema) Get(ctx context.Context, httpCli *http.Client) (json.RawMessage, error) {
if s.Value != "" {
return json.RawMessage(s.Value), nil
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.RefURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("while creating request: %w", err)
}

res, err := httpCli.Do(req)
if err != nil {
return nil, fmt.Errorf("while fetching JSON schema by RefURL %q: %w", s.RefURL, err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("while getting JSON schema: got status code: %d", res.StatusCode)
}

var out json.RawMessage
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return nil, err
}
return out, nil
}
67 changes: 58 additions & 9 deletions pkg/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ import (

"github.com/hashicorp/go-plugin"
"github.com/sirupsen/logrus"
stringutil "k8s.io/utils/strings"

"github.com/kubeshop/botkube/internal/config/remote"
"github.com/kubeshop/botkube/pkg/api"
"github.com/kubeshop/botkube/pkg/api/executor"
"github.com/kubeshop/botkube/pkg/api/source"
"github.com/kubeshop/botkube/pkg/config"
"github.com/kubeshop/botkube/pkg/formatx"
"github.com/kubeshop/botkube/pkg/httpx"
"github.com/kubeshop/botkube/pkg/multierror"
"github.com/kubeshop/botkube/pkg/templatex"
)

const (
Expand All @@ -35,6 +38,7 @@ const (
DependencyDirEnvName = "PLUGIN_DEPENDENCY_DIR"

defaultHealthCheckInterval = 10 * time.Second
printHeaderValueCharCount = 3
)

// pluginMap is the map of plugins we can dispense.
Expand All @@ -45,13 +49,19 @@ var pluginMap = map[string]plugin.Plugin{
TypeExecutor.String(): &executor.Plugin{},
}

// IndexRenderData returns plugin index render data.
type IndexRenderData struct {
Remote remote.Config `yaml:"remote"`
}

// Manager provides functionality for managing executor and source plugins.
type Manager struct {
isStarted atomic.Bool
log logrus.FieldLogger
logConfig config.Logger
cfg config.PluginManagement
httpClient *http.Client
isStarted atomic.Bool
log logrus.FieldLogger
logConfig config.Logger
cfg config.PluginManagement
httpClient *http.Client
indexRenderData IndexRenderData

sourceSupervisorChan chan pluginMetadata
executorSupervisorChan chan pluginMetadata
Expand Down Expand Up @@ -79,9 +89,15 @@ func NewManager(logger logrus.FieldLogger, logCfg config.Logger, cfg config.Plug
executorsStore := newStore[executor.Executor]()
sourcesStore := newStore[source.Source]()

remoteCfg, _ := remote.GetConfig()
indexRenderData := IndexRenderData{
Remote: remoteCfg,
}

return &Manager{
cfg: cfg,
httpClient: httpx.NewHTTPClient(),
indexRenderData: indexRenderData,
sourceSupervisorChan: sourceSupervisorChan,
executorSupervisorChan: executorSupervisorChan,
schedulerChan: schedulerChan,
Expand Down Expand Up @@ -310,7 +326,7 @@ func (m *Manager) loadRepositoriesMetadata(ctx context.Context, forceUpdate bool
"forceUpdate": forceUpdate,
}).Info("Downloading repository index")

err := m.fetchIndex(ctx, path, entry.URL)
err := m.fetchIndex(ctx, path, entry)
if err != nil {
return fmt.Errorf("while fetching index for %q repository with URL %q: %w", repo, entry.URL, err)
}
Expand All @@ -334,12 +350,28 @@ func (m *Manager) loadRepositoriesMetadata(ctx context.Context, forceUpdate bool
return nil
}

func (m *Manager) fetchIndex(ctx context.Context, path, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
func (m *Manager) fetchIndex(ctx context.Context, path string, repo config.PluginsRepository) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, repo.URL, http.NoBody)
if err != nil {
return fmt.Errorf("while creating request: %w", err)
}

headers, err := m.renderPluginIndexHeaders(repo.Headers)
if err != nil {
return fmt.Errorf("while rendering plugin index header: %w", err)
}

var strBuilder strings.Builder
for key, value := range headers {
strBuilder.WriteString(fmt.Sprintf("%s=%s\n", key, stringutil.ShortenString(value, printHeaderValueCharCount)))
req.Header.Set(key, value)
}

m.log.WithFields(logrus.Fields{
"headers": strBuilder.String(),
"url": repo.URL,
}).Debug("Fetching index via GET request...")

res, err := m.httpClient.Do(req)
if err != nil {
return fmt.Errorf("while executing request: %w", err)
Expand Down Expand Up @@ -367,6 +399,23 @@ func (m *Manager) fetchIndex(ctx context.Context, path, url string) error {
return nil
}

func (m *Manager) renderPluginIndexHeaders(headers map[string]string) (map[string]string, error) {
out := make(map[string]string)

errs := multierror.New()
for key, value := range headers {
renderedValue, err := templatex.RenderStringIfTemplate(value, m.indexRenderData)
if err != nil {
errs = multierror.Append(errs, fmt.Errorf("while rendering header %q: %w", key, err))
continue
}

out[key] = renderedValue
}

return out, errs.ErrorOrNil()
}

func createGRPCClients[C any](ctx context.Context, logger logrus.FieldLogger, logConfig config.Logger, pluginMeta map[string]pluginMetadata, pluginType Type, supervisorChan chan pluginMetadata, healthCheckInterval time.Duration) (*storePlugins[C], error) {
out := map[string]enabledPlugins[C]{}
for key, pm := range pluginMeta {
Expand Down Expand Up @@ -496,7 +545,7 @@ func (m *Manager) ensurePluginDownloaded(ctx context.Context, binPath string, in

err = downloadBinary(ctx, binPath, url, true)
if err != nil {
return fmt.Errorf("while downloading dependency from URL %q: %w", url, err)
return fmt.Errorf("while downloading dependency from URL %q (checksum: %q): %w", url.URL, url.Checksum, err)
}
}

Expand Down
Loading
Loading