Skip to content

Commit

Permalink
Add support for cfg via env vars and define external volumes #601
Browse files Browse the repository at this point in the history
##### ISSUE TYPE
 - Feature Pull Request

##### SUMMARY

- Allows providing communicator configuration via env variables
- Env variables have higher priority that config from file
- Helm chart has:
    - `extraEnv`
    - `extraVolumeMounts`
    - `extraVolumes`

Fixes #480

Related documentation: kubeshop/botkube-docs#82

##### TESTING

Unit test proves that the reading configuration works as expected. However, below you will find an e2e tutorial.

**BotKube with Vault via CSI driver**

1. Create K8s cluster, e.g. k3s via `lima-vm`: `limactl start template://k3s`
    > **NOTE:** The CSI needs to be supported, on k3d is problematic: k3d-io/k3d#206. Alternative is to just not play with the CSI driver and create your own volume that will be mounted, e.g. with predefined secret.
2. Install Vault:
    ```bash
    helm repo add hashicorp https://helm.releases.hashicorp.com
    helm repo update
    helm install vault hashicorp/vault \
        --set "server.dev.enabled=true" \
        --set "injector.enabled=false" \
        --set "csi.enabled=true"
    ```
3. Set Slack token:
    ```bash
    kubectl exec -it vault-0 -- /bin/sh
    ```
    ```bash
    vault kv put secret/slack token={token}
    ```
4. Configure Kubernetes authentication:
    ```bash
    vault auth enable kubernetes
    vault write auth/kubernetes/config \
        kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
    ```
    ```bash
    vault policy write internal-app - <<EOF
    path "secret/data/slack" {
      capabilities = ["read"]
    }
    EOF
    ```
    ```bash
    vault write auth/kubernetes/role/database \
        bound_service_account_names=botkube-sa \
        bound_service_account_namespaces=default \
        policies=internal-app \
        ttl=20m
    ```
5. Install the secrets store CSI driver:
    ```bash
    helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
    helm install csi secrets-store-csi-driver/secrets-store-csi-driver --set syncSecret.enabled=true
    ```
6. Create install parameters:
    
    ```bash
    cat > /tmp/values.yaml << ENDOFFILE
    extraObjects:
      - apiVersion: secrets-store.csi.x-k8s.io/v1
        kind: SecretProviderClass
        metadata:
          name: vault-database
        spec:
          provider: vault
          secretObjects:
            - data:
                - key: token
                  objectName: "slack-token"
              secretName: communication-slack
              type: Opaque
          parameters:
            vaultAddress: "http://vault.default:8200"
            roleName: "database"
            objects: |
              - objectName: "slack-token"
                secretPath: "secret/data/slack"
                secretKey: "token"
    
    communications:
      # Settings for Slack
      slack:
        enabled: true
        channel: 'random'
        notiftype: short
        # token - specified via env
    
    extraEnv:
      - name: COMMUNICATION_SLACK_TOKEN
        valueFrom:
          secretKeyRef:
            name: communication-slack
            key: token
    
    extraVolumeMounts:
      - name: secrets-store-inline
        mountPath: "/mnt/secrets-store"
        readOnly: true
    
    extraVolumes:
      - name: secrets-store-inline
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: "vault-database"
    image:
      registry: mszostok
      repository: botkube
      tag: env-test-v2
    ENDOFFILE
    ```
7. Checkout this PR: `gh pr checkout 601`
8. Install BotKube:
    ```bash
    helm install botkube -f /tmp/values.yaml ./helm/botkube
    ```
  • Loading branch information
mszostok committed Jun 9, 2022
1 parent 98c1123 commit ab86a23
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 20 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/hashicorp/go-multierror v1.1.1
github.com/infracloudio/msbotbuilder-go v0.2.5
github.com/knadh/koanf v1.4.1
github.com/larksuite/oapi-sdk-go v1.1.44
github.com/mattermost/mattermost-server/v5 v5.39.3
github.com/olivere/elastic v6.2.37+incompatible
Expand Down Expand Up @@ -86,8 +87,11 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.11 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand Down
43 changes: 43 additions & 0 deletions go.sum

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion helm/botkube/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ serviceAccount:
| containerSecurityContext.privileged | bool | `false` | |
| containerSecurityContext.readOnlyRootFilesystem | bool | `true` | |
| extraAnnotations | object | `{}` | |
| extraEnv | string | `nil` | |
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.registry | string | `"ghcr.io"` | |
Expand Down Expand Up @@ -119,3 +118,6 @@ serviceAccount:
| serviceMonitor.port | string | `"metrics"` | |
| tolerations | list | `[]` | |
| extraObjects | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. |
| extraEnv | list | `[]` | Extra environment variables to pass to the BotKube container. For the syntax, see the [environment variable API](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables) document. |
| extraVolumeMounts | list | `[]` | Extra volume mounts to pass to the BotKube container. For the syntax, see the [Volumes API](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1) document. |
| extraVolumes | list | `[]` | Extra volumes to pass to the BotKube container. Mount it later with extraVolumeMounts. For the syntax, see the [mount Volumes API](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume/#Volume) document. |
13 changes: 8 additions & 5 deletions helm/botkube/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ spec:
volumeMounts:
- name: config-volume
mountPath: "/config"
{{- with .Values.extraVolumeMounts }}
{{ toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.config.ssl.enabled }}
- name: certs
mountPath: "/etc/ssl/certs"
Expand All @@ -73,12 +76,9 @@ spec:
- name: KUBECONFIG
value: "/.kube/config"
{{- end }}
{{- if .Values.extraEnv }}
{{- range $name, $value := .Values.extraEnv }}
- name: {{ $name }}
value: {{ quote $value }}
{{- with .Values.extraEnv }}
{{ toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if .Values.resources }}
resources:
{{ toYaml .Values.resources | indent 12 }}
Expand All @@ -91,6 +91,9 @@ spec:
name: {{ include "botkube.fullname" . }}-configmap
- secret:
name: {{ include "botkube.CommunicationsSecretName" . }}
{{- with .Values.extraVolumes }}
{{ toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.config.ssl.enabled }}
- name: certs
secret:
Expand Down
58 changes: 49 additions & 9 deletions helm/botkube/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ service:
port: 2112
targetPort: 2112

# Ingress settings to expose teams and lark endpoints
## Ingress settings to expose teams and lark endpoints
ingress:
create: false
annotations:
Expand Down Expand Up @@ -413,10 +413,50 @@ resources: {}
# cpu: 100m
# memory: 128Mi

## Extra environment variables to pass to the BotKube container.
## ref: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables
extraEnv: []
## Extra environment variables to pass to the BotKube container, example HTTP_PROXY
## extraEnv:
## HTTP_PROXY: <proxyURL>:<port>
# - name: <key>
# valueFrom:
# configMapKeyRef:
# name: configmap-name
# key: value_key
# - name: <key>
# value: value


## Extra volumes to pass to the BotKube container. Mount it later with extraVolumeMounts.
## ref: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume/#Volume
extraVolumes: []
# - name: extra-volume-0
# secret:
# secretName: <secret-name>
#
## For CSI e.g. Vault:
# - name: secrets-store-inline
# csi:
# driver: secrets-store.csi.k8s.io
# readOnly: true
# volumeAttributes:
# secretProviderClass: "vault-database"

## Extra volume mounts to pass to the BotKube container.
## ref: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1
extraVolumeMounts: []
# - name: extra-volume-0
# mountPath: /mnt/volume0
# readOnly: true
# - name: extra-volume-1
# mountPath: /mnt/volume1
# readOnly: true
# - name: secret-files
# mountPath: /etc/secrets
# subPath: ""
#
## For CSI e.g. Vault:
# - name: secrets-store-inline
# mountPath: "/mnt/secrets-store"
# readOnly: true

nodeSelector: {}

Expand All @@ -439,12 +479,12 @@ serviceAccount:
# annotations for the service account
annotations: {}

# Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources.
## Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources.
extraObjects: []
# For example, to create a ClusterRoleBinding resource without creating a dedicated ClusterRole, uncomment the following snippet.
# NOTE: While running Helm install/upgrade with this sample snippet uncommented, make sure to set the following values:
# 1. `rbac.create: false`
# 2.`extraClusterRoleName: {clusterRole}`, where {clusterRole} is a given ClusterRole name (e.g. `cluster-admin`).
## For example, to create a ClusterRoleBinding resource without creating a dedicated ClusterRole, uncomment the following snippet.
## NOTE: While running Helm install/upgrade with this sample snippet uncommented, make sure to set the following values:
## 1. `rbac.create: false`
## 2.`extraClusterRoleName: {clusterRole}`, where {clusterRole} is a given ClusterRole name (e.g. `cluster-admin`).
#
# - apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRoleBinding
Expand Down
38 changes: 33 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ package config
import (
"os"
"path/filepath"
"strings"

"github.com/knadh/koanf"
koanfyaml "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -69,6 +74,9 @@ const (
DiscordBot BotPlatform = "discord"
// LarkBot bot platform
LarkBot BotPlatform = "lark"

communicationEnvVariablePrefix = "COMMUNICATIONS_"
communicationConfigDelimiter = "."
)

// EventType to watch
Expand Down Expand Up @@ -253,19 +261,39 @@ func (eventType EventType) String() string {
}

// NewCommunicationsConfig return new communication config object
func NewCommunicationsConfig() (c *Communications, err error) {
func NewCommunicationsConfig() (*Communications, error) {
configPath := os.Getenv("CONFIG_PATH")
commCfgFilePath := filepath.Join(configPath, CommunicationConfigFileName)
rawCfg, err := os.ReadFile(filepath.Clean(commCfgFilePath))

k := koanf.New(communicationConfigDelimiter)

// Load base YAML config.
if err := k.Load(file.Provider(filepath.Clean(commCfgFilePath)), koanfyaml.Parser()); err != nil {
return nil, err
}

// Load environment variables and merge into the loaded config.
err := k.Load(env.Provider(
communicationEnvVariablePrefix,
communicationConfigDelimiter,
normalizeCommunicationConfigEnvName,
), nil)
if err != nil {
return nil, err
}

commCfg := &Communications{}
if err := yaml.Unmarshal(rawCfg, commCfg); err != nil {
var cfg Communications
err = k.UnmarshalWithConf("", &cfg, koanf.UnmarshalConf{Tag: "yaml"})
if err != nil {
return nil, err
}
return commCfg, nil

return &cfg, nil
}

func normalizeCommunicationConfigEnvName(name string) string {
name = strings.ToLower(name)
return strings.ReplaceAll(name, "_", ".")
}

// Load loads new configuration from file.
Expand Down
62 changes: 62 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package config

import (
"fmt"
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

const sampleCommunicationConfig = "testdata/comm_config.yaml"

func TestCommunicationConfigSuccess(t *testing.T) {
t.Run("Load from file", func(t *testing.T) {
// given
t.Setenv("CONFIG_PATH", "testdata")

var expConfig Communications
loadYAMLFile(t, sampleCommunicationConfig, &expConfig)

// when
gotCfg, err := NewCommunicationsConfig()

//then
require.NoError(t, err)
require.NotNil(t, gotCfg)
assert.Equal(t, expConfig, *gotCfg)
})

t.Run("Load from file and override with environment variables", func(t *testing.T) {
// given
t.Setenv("CONFIG_PATH", "testdata")

fixToken := fmt.Sprintf("TOKEN_FROM_ENV_%d", time.Now().Unix())
t.Setenv("COMMUNICATIONS_SLACK_TOKEN", fixToken)
var expConfig Communications
loadYAMLFile(t, sampleCommunicationConfig, &expConfig)
expConfig.Communications.Slack.Token = fixToken

// when
gotCfg, err := NewCommunicationsConfig()

//then
require.NoError(t, err)
require.NotNil(t, gotCfg)
assert.Equal(t, expConfig, *gotCfg)
})
}

func loadYAMLFile(t *testing.T, path string, out interface{}) {
t.Helper()

raw, err := os.ReadFile(filepath.Clean(path))
require.NoError(t, err)

err = yaml.Unmarshal(raw, out)
require.NoError(t, err)
}
57 changes: 57 additions & 0 deletions pkg/config/testdata/comm_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Channels configuration
communications:
# Settings for Slack
slack:
enabled: false
channel: 'SLACK_CHANNEL'
token: 'SLACK_API_TOKEN'
notiftype: short # Change notification type short/long you want to receive. notiftype is optional and Default notification type is short (if not specified)

# Settings for Mattermost
mattermost:
enabled: false
url: 'MATTERMOST_SERVER_URL' # URL where Mattermost is running. e.g https://example.com:9243
token: 'MATTERMOST_TOKEN' # Personal Access token generated by BotKube user
team: 'MATTERMOST_TEAM' # Mattermost Team to configure with BotKube
channel: 'MATTERMOST_CHANNEL' # Mattermost Channel for receiving BotKube alerts
notiftype: short # Change notification type short/long you want to receive. notiftype is optional and Default notification type is short (if not specified)

# Settings for MS Teams
teams:
enabled: false
appID: 'APPLICATION_ID'
appPassword: 'APPLICATION_PASSWORD'
notiftype: short
port: 3978

# Settings for Discord
discord:
enabled: false
token: 'DISCORD_TOKEN' # BotKube Bot Token
botid: 'DISCORD_BOT_ID' # BotKube Application Client ID
channel: 'DISCORD_CHANNEL_ID' # Discord Channel id for receiving BotKube alerts
notiftype: short # Change notification type short/long you want to receive. notiftype is optional and Default notification type is short (if not specified)


# Settings for ELS
elasticsearch:
enabled: false
awsSigning:
enabled: false # enable awsSigning using IAM for Elastisearch hosted on AWS, if true make sure AWS environment variables are set. Refer https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
awsRegion: 'us-east-1' # AWS region where Elasticsearch is deployed
roleArn: '' # AWS IAM Role arn to assume for credentials, use this only if you dont want to use the EC2 instance role or not running on AWS instance
server: 'ELASTICSEARCH_ADDRESS' # e.g https://example.com:9243
username: 'ELASTICSEARCH_USERNAME' # Basic Auth
password: 'ELASTICSEARCH_PASSWORD'
skipTLSVerify: false # toggle verification of TLS certificate of the Elastic nodes. Verification is skipped when option is true. Enable to connect to clusters with self-signed certs
# ELS index settings
index:
name: botkube
type: botkube-event
shards: 1
replicas: 0

# Settings for Webhook
webhook:
enabled: false
url: 'WEBHOOK_URL' # e.g https://example.com:80

0 comments on commit ab86a23

Please sign in to comment.