Skip to content

Commit

Permalink
Remove global variables, pass context across all components and impro…
Browse files Browse the repository at this point in the history
…ve error handling #603

##### ISSUE TYPE
 - Feature Pull Request

##### SUMMARY
- Use envconfig
- Remove global variables related to Kubernetes, logger, and filters
- Pass context across all components - support graceful shutdown of all BotKube components
- Improve error handling (return errors in almost all places instead of ignoring/logging them)

This is just a start, as later (during new features development) we should refactor each component/package in isolation. But after this huge PR it should be possible to do it.

Fixes #220 

##### TODO

After first review of this PR if the new filter engine approach is accepted: 
- [x] Update documentation (https://www.botkube.io/filters/) PR: kubeshop/botkube-docs#81

##### TESTING

Tested manually with Mattermost, Discord and Slack.
You can use the following image:`pkosiec/botkube:remove-global-vars-v2`
  • Loading branch information
pkosiec committed Jun 6, 2022
1 parent 7c7c533 commit 8314d73
Show file tree
Hide file tree
Showing 60 changed files with 2,131 additions and 1,616 deletions.
12 changes: 9 additions & 3 deletions CONTRIBUTING.md
Expand Up @@ -92,16 +92,22 @@ For faster development, you can also build and run BotKube outside K8s cluster.
# From project root directory
$ export CONFIG_PATH=$(pwd)
```
4. Make sure that correct context is set and you are able to access your Kubernetes cluster
4. Export the path to Kubeconfig:

```sh
export KUBECONFIG=/Users/$USER/.kube/config # set custom path if necessary
```

5. Make sure that correct context is set and you are able to access your Kubernetes cluster
```console
$ kubectl config current-context
minikube
$ kubectl cluster-info
Kubernetes master is running at https://192.168.39.233:8443
CoreDNS is running at https://192.168.39.233:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
...
```
5. Run BotKube binary
```
6. Run BotKube binary
```sh
$ ./botkube
```
Expand Down
187 changes: 140 additions & 47 deletions cmd/botkube/main.go
Expand Up @@ -20,113 +20,206 @@
package main

import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/google/go-github/v44/github"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
"github.com/vrischmann/envconfig"
"golang.org/x/sync/errgroup"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"

"github.com/infracloudio/botkube/pkg/bot"
"github.com/infracloudio/botkube/pkg/config"
"github.com/infracloudio/botkube/pkg/controller"
"github.com/infracloudio/botkube/pkg/execute"
"github.com/infracloudio/botkube/pkg/filterengine"
"github.com/infracloudio/botkube/pkg/log"
"github.com/infracloudio/botkube/pkg/metrics"
"github.com/infracloudio/botkube/pkg/httpsrv"
"github.com/infracloudio/botkube/pkg/kube"
"github.com/infracloudio/botkube/pkg/notify"
"github.com/infracloudio/botkube/pkg/utils"
)

// Config contains the app configuration parameters.
type Config struct {
MetricsPort string `envconfig:"default=2112"`
LogLevel string `envconfig:"default=error"`
ConfigPath string `envconfig:"optional"`
InformersResyncPeriod time.Duration `envconfig:"default=30m"`
KubeconfigPath string `envconfig:"optional,KUBECONFIG"`
}

const (
defaultMetricsPort = "2112"
componentLogFieldKey = "component"
botLogFieldKey = "bot"
)

// TODO:
// - Use context to make sure all goroutines shutdowns gracefully: https://github.com/infracloudio/botkube/issues/220
// - Make the code testable (shorten methods and functions, and reduce level of cyclomatic complexity): https://github.com/infracloudio/botkube/issues/589

func main() {
// Prometheus metrics
metricsPort, exists := os.LookupEnv("METRICS_PORT")
if !exists {
metricsPort = defaultMetricsPort
}
var appCfg Config
err := envconfig.Init(&appCfg)
exitOnError(err, "while loading app configuration")

// Set up global logger and filter engine
log.SetupGlobal()
filterengine.SetupGlobal()
logger := newLogger(appCfg.LogLevel)
ctx := signals.SetupSignalHandler()
ctx, cancelCtxFn := context.WithCancel(ctx)
defer cancelCtxFn()

errGroup := new(errgroup.Group)
errGroup, ctx := errgroup.WithContext(ctx)

// Prometheus metrics
metricsSrv := newMetricsServer(logger.WithField(componentLogFieldKey, "Metrics server"), appCfg.MetricsPort)
errGroup.Go(func() error {
return metrics.ServeMetrics(metricsPort)
return metricsSrv.Serve(ctx)
})

log.Info("Starting controller")

conf, err := config.New()
conf, err := config.Load(appCfg.ConfigPath)
exitOnError(err, "while loading configuration")
if conf == nil {
log.Fatal("while loading configuration: config cannot be nil")
}

// List notifiers
notifiers := notify.ListNotifiers(conf.Communications)
// Prepare K8s clients and mapper
dynamicCli, discoveryCli, mapper, err := kube.SetupK8sClients(appCfg.KubeconfigPath)
exitOnError(err, "while initializing K8s clients")

// Set up the filter engine
filterEngine := filterengine.WithAllFilters(logger, dynamicCli, mapper, conf)

// List notifiers
notifiers, err := notify.LoadNotifiers(logger, conf.Communications)
exitOnError(err, "while loading notifiers")

// Create Executor Factory
resMapping, err := execute.LoadResourceMappingIfShould(
logger.WithField(componentLogFieldKey, "Resource Mapping Loader"),
conf,
discoveryCli,
)
exitOnError(err, "while loading resource mapping")

executorFactory := execute.NewExecutorFactory(
logger.WithField(componentLogFieldKey, "Executor"),
execute.DefaultCommandRunnerFunc,
*conf,
filterEngine,
resMapping,
)

// Run bots
if conf.Communications.Slack.Enabled {
log.Info("Starting slack bot")
sb := bot.NewSlackBot(conf)
sb := bot.NewSlackBot(logger.WithField(botLogFieldKey, "Slack"), conf, executorFactory)
errGroup.Go(func() error {
return sb.Start()
return sb.Start(ctx)
})
}

if conf.Communications.Mattermost.Enabled {
log.Info("Starting mattermost bot")
mb := bot.NewMattermostBot(conf)
mb := bot.NewMattermostBot(logger.WithField(botLogFieldKey, "Mattermost"), conf, executorFactory)
errGroup.Go(func() error {
return mb.Start()
return mb.Start(ctx)
})
}

if conf.Communications.Teams.Enabled {
log.Info("Starting MS Teams bot")
tb := bot.NewTeamsBot(conf)
tb := bot.NewTeamsBot(logger.WithField(botLogFieldKey, "MS Teams"), conf, executorFactory)
// TODO: Unify that with other notifiers: Split this into two structs or merge other bots and notifiers into single structs
notifiers = append(notifiers, tb)
errGroup.Go(func() error {
return tb.Start()
return tb.Start(ctx)
})
}

if conf.Communications.Discord.Enabled {
log.Info("Starting discord bot")
db := bot.NewDiscordBot(conf)
db := bot.NewDiscordBot(logger.WithField(botLogFieldKey, "Discord"), conf, executorFactory)
errGroup.Go(func() error {
return db.Start()
return db.Start(ctx)
})
}

if conf.Communications.Lark.Enabled {
log.Info("Starting lark bot")
lb := bot.NewLarkBot(conf)
lb := bot.NewLarkBot(logger.WithField(botLogFieldKey, "Lark"), logger.GetLevel(), conf, executorFactory)
errGroup.Go(func() error {
return lb.Start()
return lb.Start(ctx)
})
}

// Start upgrade notifier
// Start upgrade checker
ghCli := github.NewClient(&http.Client{
Timeout: 1 * time.Minute,
})
if conf.Settings.UpgradeNotifier {
log.Info("Starting upgrade notifier")
upgradeChecker := controller.NewUpgradeChecker(
logger.WithField(componentLogFieldKey, "Upgrade Checker"),
notifiers,
ghCli.Repositories,
)
errGroup.Go(func() error {
controller.UpgradeNotifier(notifiers)
return nil
return upgradeChecker.Run(ctx)
})
}

// Init KubeClient, InformerMap and start controller
utils.InitKubeClient()
utils.InitInformerMap(conf)
utils.InitResourceMap(conf)
controller.RegisterInformers(conf, notifiers)
// Start Config Watcher
if conf.Settings.ConfigWatcher {
cfgWatcher := controller.NewConfigWatcher(
logger.WithField(componentLogFieldKey, "Config Watcher"),
appCfg.ConfigPath,
conf.Settings.ClusterName,
notifiers,
)
errGroup.Go(func() error {
return cfgWatcher.Do(ctx, cancelCtxFn)
})
}

// Start controller

ctrl := controller.New(
logger.WithField(componentLogFieldKey, "Controller"),
conf,
notifiers,
filterEngine,
appCfg.ConfigPath,
dynamicCli,
mapper,
appCfg.InformersResyncPeriod,
)

err = ctrl.Start(ctx)
exitOnError(err, "while starting controller")

err = errGroup.Wait()
exitOnError(err, "while waiting for goroutines to finish gracefully")
}

func newLogger(logLevelStr string) *logrus.Logger {
logger := logrus.New()
// Output to stdout instead of the default stderr
logger.SetOutput(os.Stdout)

// Only logger the warning severity or above.
logLevel, err := logrus.ParseLevel(logLevelStr)
if err != nil {
// Set Info level as a default
logLevel = logrus.InfoLevel
}
logger.SetLevel(logLevel)
logger.Formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true}

return logger
}

func newMetricsServer(log logrus.FieldLogger, metricsPort string) *httpsrv.Server {
addr := fmt.Sprintf(":%s", metricsPort)
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())

return httpsrv.New(log, addr, mux)
}

func exitOnError(err error, context string) {
if err != nil {
log.Fatalf("%s: %v", context, err)
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Expand Up @@ -5,6 +5,7 @@ require (
github.com/bwmarrin/discordgo v0.25.0
github.com/fsnotify/fsnotify v1.5.4
github.com/google/go-github/v44 v44.1.0
github.com/hashicorp/go-multierror v1.1.1
github.com/infracloudio/msbotbuilder-go v0.2.5
github.com/larksuite/oapi-sdk-go v1.1.44
github.com/mattermost/mattermost-server/v5 v5.39.3
Expand All @@ -14,13 +15,15 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/slack-go/slack v0.10.3
github.com/stretchr/testify v1.7.1
github.com/vrischmann/envconfig v1.3.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/api v0.24.0
k8s.io/apimachinery v0.24.0
k8s.io/client-go v0.24.0
k8s.io/kubectl v0.24.0
k8s.io/sample-controller v0.24.0
sigs.k8s.io/controller-runtime v0.12.1
)

require (
Expand Down Expand Up @@ -61,7 +64,6 @@ require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
Expand Down Expand Up @@ -112,7 +114,7 @@ require (
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.8.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
Expand All @@ -135,7 +137,7 @@ require (
sigs.k8s.io/kustomize/api v0.11.4 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

go 1.18

0 comments on commit 8314d73

Please sign in to comment.