0
0
Fork 0
mirror of https://github.com/crazy-max/diun.git synced 2025-04-11 14:11:21 +00:00

Allow to define namespaces for Kubernetes

Use annotations instead of labels for Kubernetes
Add Kubernetes provider doc
This commit is contained in:
CrazyMax 2020-06-17 02:22:50 +02:00 committed by CrazyMax
parent 2a936199ef
commit dc5949819b
12 changed files with 395 additions and 40 deletions

View file

@ -11,9 +11,26 @@ services:
- "TZ=Europe/Paris"
- "LOG_LEVEL=info"
- "LOG_JSON=false"
- "DIUN_WATCH_WORKERS=20"
- "DIUN_WATCH_SCHEDULE=*/30 * * * *"
- "DIUN_PROVIDERS_DOCKER=true"
- "DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=false"
- "DIUN_PROVIDERS_DOCKER_WATCHSTOPPED=false"
labels:
- "diun.enable=true"
- "diun.watch_repo=true"
restart: always
cloudflared:
image: crazymax/cloudflared:latest
ports:
- target: 5053
published: 5053
protocol: udp
- target: 49312
published: 49312
protocol: tcp
environment:
- "TZ=Europe/Paris"
- "TUNNEL_DNS_UPSTREAM=https://1.1.1.1/dns-query,https://1.0.0.1/dns-query"
labels:
- "diun.enable=true"
- "diun.watch_repo=true"

47
.examples/k8s/diun.yml Normal file
View file

@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: diun
spec:
replicas: 1
selector:
matchLabels:
app: diun
template:
metadata:
labels:
app: diun
# annotations:
# diun.enable: "true"
# diun.watch_repo: "true"
spec:
containers:
- name: diun
image: crazymax/diun:latest
imagePullPolicy: Always
env:
- name: TZ
value: "Europe/Paris"
- name: LOG_LEVEL
value: "info"
- name: LOG_JSON
value: "false"
- name: DIUN_WATCH_WORKERS
value: "20"
- name: DIUN_WATCH_SCHEDULE
value: "*/30 * * * *"
- name: DIUN_PROVIDERS_KUBERNETES
value: "true"
volumeMounts:
- mountPath: "/data"
name: "data"
restartPolicy: Always
volumes:
# Set up a data directory for gitea
# For production usage, you should consider using PV/PVC instead(or simply using storage like NAS)
# For more details, please see https://kubernetes.io/docs/concepts/storage/volumes/
- name: "data"
hostPath:
path: "/data"
type: Directory

22
.examples/k8s/nginx.yml Normal file
View file

@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
run: nginx
replicas: 2
template:
metadata:
labels:
run: nginx
annotations:
diun.enable: "true"
diun.watch_repo: "true"
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80

View file

@ -97,6 +97,10 @@ providers:
watchStopped: true
swarm:
watchByDefault: true
kubernetes:
namespaces:
- default
- production
file:
directory: ./imagesdir
```
@ -157,4 +161,5 @@ You can also use the following environment variables:
* [docker](providers/docker.md)
* [swarm](providers/swarm.md)
* [kubernetes](providers/kubernetes.md)
* [file](providers/file.md)

View file

@ -20,7 +20,7 @@ docker-compose exec diun --test-notif
## field docker|swarm uses unsupported type: invalid
If you have the error `failed to decode configuration from file: field docker uses unsupported type: invalid` that's because your `docker` or `swarm` provider is not initialized in your configuration:
If you have the error `failed to decode configuration from file: field docker uses unsupported type: invalid` that's because your `docker`, `swarm` or `kubernetes` provider is not initialized in your configuration:
```yaml
providers:

214
doc/providers/kubernetes.md Normal file
View file

@ -0,0 +1,214 @@
# Kubernetes provider
* [About](#about)
* [Quick start](#quick-start)
* [Provider configuration](#provider-configuration)
* [Configuration file](#configuration-file)
* [Environment variables](#environment-variables)
* [Kubernetes annotations](#kubernetes-annotations)
## About
The Kubernetes provider allows you to analyze the pods of your Kubernetes cluster to extract images found and check for updates on the registry.
## Quick start
In this section we quickly go over a basic deployment using your local Kubernetes cluster.
Here we use our local Kubernetes provider with a minimum configuration to analyze annotated pods (watch by default disabled).
Now let's create a simple pod for Diun:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: diun
spec:
replicas: 1
selector:
matchLabels:
app: diun
template:
metadata:
labels:
app: diun
spec:
containers:
- name: diun
image: crazymax/diun:latest
imagePullPolicy: Always
env:
- name: TZ
value: "Europe/Paris"
- name: LOG_LEVEL
value: "info"
- name: LOG_JSON
value: "false"
- name: DIUN_WATCH_WORKERS
value: "20"
- name: DIUN_WATCH_SCHEDULE
value: "*/30 * * * *"
- name: DIUN_PROVIDERS_KUBERNETES
value: "true"
volumeMounts:
- mountPath: "/data"
name: "data"
restartPolicy: Always
volumes:
# Set up a data directory for gitea
# For production usage, you should consider using PV/PVC instead(or simply using storage like NAS)
# For more details, please see https://kubernetes.io/docs/concepts/storage/volumes/
- name: "data"
hostPath:
path: "/data"
type: Directory
```
And another one with a simple Nginx pod:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
run: nginx
replicas: 2
template:
metadata:
labels:
run: nginx
annotations:
diun.enable: "true"
diun.watch_repo: "true"
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
```
As an example we use [nginx](https://hub.docker.com/_/nginx/) Docker image. A few [annotations](#kubernetes-annotations) are added to configure the image analysis of this pod for Diun. We can now start these 2 pods:
```
kubectl apply -f diun.yml
kubectl apply -f nginx.yml
```
Now take a look at the logs:
```
$ kubectl logs -f -l app=diun --all-containers
# TODO: add logs example
```
## Provider configuration
### Configuration file
#### `endpoint`
The Kubernetes server endpoint as URL.
```yaml
providers:
kubernetes:
endpoint: "http://localhost:8080"
```
Kubernetes server endpoint as URL, which is only used when the behavior based on environment variables described below does not apply.
When deployed into Kubernetes, Diun reads the environment variables `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` or `KUBECONFIG` to create the endpoint.
The access token is looked up in `/var/run/secrets/kubernetes.io/serviceaccount/token` and the SSL CA certificate in `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`. They are both provided automatically as mounts in the pod where Diun is deployed.
When the environment variables are not found, Diun tries to connect to the Kubernetes API server with an external-cluster client. In which case, the endpoint is required. Specifically, it may be set to the URL used by `kubectl proxy` to connect to a Kubernetes cluster using the granted authentication and authorization of the associated kubeconfig.
#### `token`
```yaml
providers:
kubernetes:
token: "atoken"
```
Bearer token used for the Kubernetes client configuration.
#### `tokenFile`
Use content of secret file as bearer token if `token` not defined.
```yaml
providers:
kubernetes:
tokenFile: "/run/secrets/token"
```
#### `certAuthFilePath`
Path to the certificate authority file. Used for the Kubernetes client configuration.
```yaml
providers:
kubernetes:
certAuthFilePath: "/a/ca.crt"
```
#### `tlsInsecure`
Controls whether client does not verify the server's certificate chain and hostname (default `false`).
```yaml
providers:
kubernetes:
tlsInsecure: false
```
#### `namespaces`
Array of namespaces to watch (default all namespaces).
```yaml
providers:
kubernetes:
namespaces:
- default
- production
```
#### `watchByDefault`
Enable watch by default. If false, pods that don't have `diun.enable: "true"` annotation will be ignored (default `false`).
```yaml
providers:
kubernetes:
watchByDefault: false
```
### Environment variables
* `DIUN_PROVIDERS_KUBERNETES`
* `DIUN_PROVIDERS_KUBERNETES_ENDPOINT`
* `DIUN_PROVIDERS_KUBERNETES_TOKEN`
* `DIUN_PROVIDERS_KUBERNETES_TOKENFILE`
* `DIUN_PROVIDERS_KUBERNETES_CERTAUTHFILEPATH`
* `DIUN_PROVIDERS_KUBERNETES_TLSINSECURE`
* `DIUN_PROVIDERS_KUBERNETES_NAMESPACES` (comma separated)
* `DIUN_PROVIDERS_KUBERNETES_WATCHBYDEFAULT`
## Kubernetes annotations
You can configure more finely the way to analyze the image of your pods through Kubernetes annotations:
* `diun.enable`: Set to true to enable image analysis of this pod.
* `diun.regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
* `diun.watch_repo`: Watch all tags of this pod image (default `false`).
* `diun.max_tags`: Maximum number of tags to watch if `diun.watch_repo` enabled. 0 means all of them (default `0`).
* `diun.include_tags`: Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo`.
* `diun.exclude_tags`: Semi-colon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo`.

View file

@ -6,13 +6,13 @@ import (
// PrdKubernetes holds kubernetes provider configuration
type PrdKubernetes struct {
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty" validate:"omitempty"`
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"`
TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"`
ConfigFile string `yaml:"configFile" json:"configFile,omitempty" validate:"omitempty,file"`
TLSCAFile string `yaml:"tlsCaFile" json:"tlsCaFile,omitempty" validate:"omitempty"`
TLSInsecure *bool `yaml:"tlsInsecure" json:"tlsInsecure,omitempty" validate:"required"`
WatchByDefault *bool `yaml:"watchByDefault" json:"watchByDefault,omitempty" validate:"required"`
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty" validate:"omitempty"`
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"`
TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"`
CertAuthFilePath string `yaml:"certAuthFilePath" json:"certAuthFilePath,omitempty" validate:"omitempty"`
TLSInsecure *bool `yaml:"tlsInsecure" json:"tlsInsecure,omitempty" validate:"required"`
Namespaces []string `yaml:"namespaces" json:"namespaces,omitempty" validate:"omitempty"`
WatchByDefault *bool `yaml:"watchByDefault" json:"watchByDefault,omitempty" validate:"required"`
}
// GetDefaults gets the default values

View file

@ -19,7 +19,7 @@ func New(config *model.PrdKubernetes) *provider.Client {
return &provider.Client{
Handler: &Client{
config: config,
logger: log.With().Str("provider", "k8s").Logger(),
logger: log.With().Str("provider", "kubernetes").Logger(),
},
}
}
@ -40,7 +40,7 @@ func (c *Client) ListJob() []model.Job {
var list []model.Job
for _, image := range images {
list = append(list, model.Job{
Provider: "k8s",
Provider: "kubernetes",
Image: image,
})
}

View file

@ -11,11 +11,12 @@ import (
func (c *Client) listPodImage() []model.Image {
cli, err := k8s.New(k8s.Options{
Endpoint: c.config.Endpoint,
Token: c.config.Token,
TokenFile: c.config.TokenFile,
TLSCAFile: c.config.TLSCAFile,
TLSInsecure: c.config.TLSInsecure,
Endpoint: c.config.Endpoint,
Token: c.config.Token,
TokenFile: c.config.TokenFile,
CertAuthFilePath: c.config.CertAuthFilePath,
TLSInsecure: c.config.TLSInsecure,
Namespaces: c.config.Namespaces,
})
if err != nil {
c.logger.Error().Err(err).Msg("Cannot create Kubernetes client")
@ -31,7 +32,7 @@ func (c *Client) listPodImage() []model.Image {
var list []model.Image
for _, pod := range pods {
for _, ctn := range pod.Spec.Containers {
image, err := provider.ValidateContainerImage(ctn.Image, pod.Labels, *c.config.WatchByDefault)
image, err := provider.ValidateContainerImage(ctn.Image, pod.Annotations, *c.config.WatchByDefault)
if err != nil {
c.logger.Error().Err(err).Msgf("Cannot get image from container %s (pod %s)", ctn.Name, pod.Name)
continue

View file

@ -8,6 +8,7 @@ import (
"github.com/crazy-max/diun/v4/pkg/utl"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
@ -15,39 +16,46 @@ import (
// Client represents an active kubernetes object
type Client struct {
ctx context.Context
API *kubernetes.Clientset
ctx context.Context
namespaces []string
API *kubernetes.Clientset
}
// Options holds kubernetes client object options
type Options struct {
Endpoint string
Token string
TokenFile string
TLSCAFile string
TLSInsecure *bool
Endpoint string
Token string
TokenFile string
CertAuthFilePath string
TLSInsecure *bool
Namespaces []string
}
// New initializes a new Kubernetes client
func New(opts Options) (*Client, error) {
var err error
var cl *kubernetes.Clientset
var api *kubernetes.Clientset
switch {
case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "":
log.Debug().Msgf("Creating in-cluster Kubernetes provider client %s", opts.Endpoint)
cl, err = newInClusterClient(opts)
api, err = newInClusterClient(opts)
case os.Getenv("KUBECONFIG") != "":
log.Debug().Msgf("Creating cluster-external Kubernetes provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG"))
cl, err = newExternalClusterClientFromFile(opts, os.Getenv("KUBECONFIG"))
api, err = newExternalClusterClientFromFile(opts, os.Getenv("KUBECONFIG"))
default:
log.Debug().Msgf("Creating cluster-external Kubernetes provider client %s", opts.Endpoint)
cl, err = newExternalClusterClient(opts)
api, err = newExternalClusterClient(opts)
}
if len(opts.Namespaces) == 0 {
opts.Namespaces = []string{metav1.NamespaceAll}
}
return &Client{
ctx: context.Background(),
API: cl,
ctx: context.Background(),
namespaces: opts.Namespaces,
API: api,
}, err
}
@ -76,7 +84,6 @@ func newExternalClusterClientFromFile(opts Options, file string) (*kubernetes.Cl
configFromFlags.TLSClientConfig.Insecure = *opts.TLSInsecure
}
configFromFlags.TLSClientConfig.Insecure = true
return kubernetes.NewForConfig(configFromFlags)
}
@ -97,8 +104,8 @@ func newExternalClusterClient(opts Options) (*kubernetes.Clientset, error) {
BearerToken: opts.Token,
}
if opts.TLSCAFile != "" {
caData, err := ioutil.ReadFile(opts.TLSCAFile)
if opts.CertAuthFilePath != "" {
caData, err := ioutil.ReadFile(opts.CertAuthFilePath)
if err != nil {
return nil, errors.Wrap(err, "Failed to read CA file")
}

View file

@ -9,14 +9,19 @@ import (
// PodList returns Kubernetes pods
func (c *Client) PodList(opts metav1.ListOptions) ([]v1.Pod, error) {
pods, err := c.API.CoreV1().Pods("").List(c.ctx, opts)
if err != nil {
return nil, err
var podList []v1.Pod
for _, ns := range c.namespaces {
pods, err := c.API.CoreV1().Pods(ns).List(c.ctx, opts)
if err != nil {
return nil, err
}
podList = append(podList, pods.Items...)
}
sort.Slice(pods.Items, func(i, j int) bool {
return pods.Items[i].Name < pods.Items[j].Name
sort.Slice(podList, func(i, j int) bool {
return podList[i].Name < podList[j].Name
})
return pods.Items, nil
return podList, nil
}

37
pkg/k8s/pod_test.go Normal file
View file

@ -0,0 +1,37 @@
package k8s_test
import (
"fmt"
"os"
"testing"
"github.com/crazy-max/diun/v4/pkg/k8s"
"github.com/crazy-max/diun/v4/pkg/utl"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestPodList(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("Skipping testing in CI environment")
}
os.Setenv("KUBECONFIG", "./.dev/minikube.config")
kc, err := k8s.New(k8s.Options{
TLSInsecure: utl.NewTrue(),
})
assert.NoError(t, err)
assert.NotNil(t, kc)
pods, err := kc.PodList(metav1.ListOptions{})
assert.NoError(t, err)
assert.NotNil(t, pods)
assert.True(t, len(pods) > 0)
for _, pod := range pods {
for _, ctn := range pod.Spec.Containers {
fmt.Println(pod.Name, ctn.Image)
}
}
}