Skip to content

Commit

Permalink
Merge pull request #37 from jittering/feature/namespace-filter
Browse files Browse the repository at this point in the history
feature/namespace filter
  • Loading branch information
chetan authored Jul 10, 2024
2 parents d4112dc + 7468b83 commit 58ba8d7
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 64 deletions.
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ providers:
Run `traefik-kop` on your other nodes via docker-compose:

```yaml
version: "3"
services:
traefik-kop:
image: "ghcr.io/jittering/traefik-kop:latest"
Expand Down Expand Up @@ -100,8 +99,9 @@ GLOBAL OPTIONS:
--docker-host value Docker endpoint (default: "unix:///var/run/docker.sock") [$DOCKER_HOST]
--docker-config value Docker provider config (file must end in .yaml) [$DOCKER_CONFIG]
--poll-interval value Poll interval for refreshing container list (default: 60) [$KOP_POLL_INTERVAL]
--verbose Enable debug logging (default: true) [$VERBOSE, $DEBUG]
--help, -h show help (default: false)
--namespace value Namespace to process containers for [$NAMESPACE]
--verbose Enable debug logging (default: false) [$VERBOSE, $DEBUG]
--help, -h show help
--version, -V Print the version (default: false)
```
Expand Down Expand Up @@ -185,6 +185,40 @@ service port on the host and tell traefik to bind to *that* port (8088 in the
example above) in the load balancer config, not the internal port (80). This is
so that traefik can reach it over the network.

## Namespaces

traefik-kop has the ability to target containers via namespaces. Simply
configure `kop` with a namespace:

```yaml
services:
traefik-kop:
image: "ghcr.io/jittering/traefik-kop:latest"
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- "REDIS_ADDR=192.168.1.50:6379"
- "BIND_IP=192.168.1.75"
- "NAMESPACE=staging"
```

Then add the `kop.namespace` label to your target services, along with the usual traefik labels:

```yaml
services:
nginx:
image: "nginx:alpine"
restart: unless-stopped
ports:
- 8088:80
labels:
- "kop.namespace=staging"
- "traefik.enable=true"
- "traefik..."
```


## Docker API

traefik-kop expects to connect to the Docker host API via a unix socket, by
Expand Down
6 changes: 6 additions & 0 deletions bin/traefik-kop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ func flags() {
Value: 60,
EnvVars: []string{"KOP_POLL_INTERVAL"},
},
&cli.StringFlag{
Name: "namespace",
Usage: "Namespace to process containers for",
EnvVars: []string{"NAMESPACE"},
},
&cli.BoolFlag{
Name: "verbose",
Usage: "Enable debug logging",
Expand Down Expand Up @@ -141,6 +146,7 @@ func doStart(c *cli.Context) error {
DockerHost: c.String("docker-host"),
DockerConfig: c.String("docker-config"),
PollInterval: c.Int64("poll-interval"),
Namespace: c.String("namespace"),
}

setupLogging(c.Bool("verbose"))
Expand Down
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Config struct {
Pass string
DB int
PollInterval int64
Namespace string
}

type ConfigFile struct {
Expand Down
33 changes: 25 additions & 8 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (

// Copied from traefik. See docker provider package for original impl

type dockerCache struct {
client client.APIClient
list []types.Container
details map[string]types.ContainerJSON
}

// Must be 0 for unix socket?
// Non-zero throws an error
const defaultTimeout = time.Duration(0)
Expand Down Expand Up @@ -51,19 +57,30 @@ func getClientOpts(endpoint string) ([]client.Opt, error) {
}

// looks up the docker container by finding the matching service or router traefik label
func findContainerByServiceName(dc client.APIClient, svcType string, svcName string, routerName string) (types.ContainerJSON, error) {
func (dc *dockerCache) findContainerByServiceName(svcType string, svcName string, routerName string) (types.ContainerJSON, error) {
svcName = strings.TrimSuffix(svcName, "@docker")
routerName = strings.TrimSuffix(routerName, "@docker")

list, err := dc.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
return types.ContainerJSON{}, errors.Wrap(err, "failed to list containers")
}
for _, c := range list {
container, err := dc.ContainerInspect(context.Background(), c.ID)
if dc.list == nil {
var err error
dc.list, err = dc.client.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
return types.ContainerJSON{}, errors.Wrapf(err, "failed to inspect container %s", c.ID)
return types.ContainerJSON{}, errors.Wrap(err, "failed to list containers")
}
}

for _, c := range dc.list {
var container types.ContainerJSON
var ok bool
if container, ok = dc.details[c.ID]; !ok {
var err error
container, err = dc.client.ContainerInspect(context.Background(), c.ID)
if err != nil {
return types.ContainerJSON{}, errors.Wrapf(err, "failed to inspect container %s", c.ID)
}
dc.details[c.ID] = container
}

// check labels
svcNeedle := fmt.Sprintf("traefik.%s.services.%s.", svcType, svcName)
routerNeedle := fmt.Sprintf("traefik.%s.routers.%s.", svcType, routerName)
Expand Down
54 changes: 33 additions & 21 deletions docker_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,22 +174,8 @@ func loadYAMLWithEnv(yaml []byte, env map[string]string) (*compose.Config, error
return loader.Load(buildConfigDetails(dict, env))
}

func doTest(t *testing.T, file string) *testStore {
p := path.Join("fixtures", file)
f, err := os.Open(p)
assert.NoError(t, err)

b, err := io.ReadAll(f)
assert.NoError(t, err)

composeConfig, err := loadYAML(b)
assert.NoError(t, err)

store := &testStore{}

// fmt.Printf("%+v\n", composeConfig)

// convert compose services to containers
// convert compose services to containers
func createContainers(composeConfig *compose.Config) []types.Container {
containers := make([]types.Container, 0)
for _, service := range composeConfig.Services {
container := types.Container{
Expand All @@ -211,9 +197,11 @@ func doTest(t *testing.T, file string) *testStore {
container.Ports = ports
containers = append(containers, container)
}
dockerAPI.containers = containers
return containers
}

// convert compose services to containersJSON
// convert compose services to containersJSON
func createContainersJSON(composeConfig *compose.Config) map[string]types.ContainerJSON {
containersJSON := make(map[string]types.ContainerJSON)
for _, service := range composeConfig.Services {
containerJSON := types.ContainerJSON{
Expand Down Expand Up @@ -260,15 +248,39 @@ func doTest(t *testing.T, file string) *testStore {
}
containersJSON[service.Name] = containerJSON
}
dockerAPI.containersJSON = containersJSON
return containersJSON
}

func doTest(t *testing.T, file string, config *Config) *testStore {
p := path.Join("fixtures", file)
f, err := os.Open(p)
assert.NoError(t, err)

b, err := io.ReadAll(f)
assert.NoError(t, err)

composeConfig, err := loadYAML(b)
assert.NoError(t, err)

store := &testStore{}

// fmt.Printf("%+v\n", composeConfig)

dockerAPI.containers = createContainers(composeConfig)
dockerAPI.containersJSON = createContainersJSON(composeConfig)

dp := &docker.Provider{}
dp.Watch = false
dp.Endpoint = dockerEndpoint

config := &Config{
BindIP: "192.168.100.100",
if config == nil {
config = &Config{
BindIP: "192.168.100.100",
}
} else {
config.BindIP = "192.168.100.100"
}

handleConfigChange := createConfigHandler(*config, store, dp, dc)

routinesPool := safe.NewPool(context.Background())
Expand Down
25 changes: 17 additions & 8 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func Test_httpServerVersion(t *testing.T) {
}

func Test_helloWorld(t *testing.T) {
store := doTest(t, "helloworld.yml")
store := doTest(t, "helloworld.yml", nil)

assert.NotNil(t, store)
assert.NotNil(t, store.kv)
Expand All @@ -71,7 +71,7 @@ func Test_helloWorld(t *testing.T) {

func Test_helloDetect(t *testing.T) {
// both services get mapped to the same port (error case)
store := doTest(t, "hellodetect.yml")
store := doTest(t, "hellodetect.yml", nil)
assertServiceIPs(t, store, []svc{
{"hello-detect", "http", "http://192.168.100.100:5577"},
{"hello-detect2", "http", "http://192.168.100.100:5577"},
Expand All @@ -80,7 +80,7 @@ func Test_helloDetect(t *testing.T) {

func Test_helloIP(t *testing.T) {
// override ip via labels
store := doTest(t, "helloip.yml")
store := doTest(t, "helloip.yml", nil)
assertServiceIPs(t, store, []svc{
{"helloip", "http", "http://4.4.4.4:5599"},
{"helloip2", "http", "http://3.3.3.3:5599"},
Expand All @@ -89,40 +89,49 @@ func Test_helloIP(t *testing.T) {

func Test_helloNetwork(t *testing.T) {
// use ip from specific docker network
store := doTest(t, "network.yml")
store := doTest(t, "network.yml", nil)
assertServiceIPs(t, store, []svc{
{"hello1", "http", "http://10.10.10.5:5555"},
})
}

func Test_TCP(t *testing.T) {
// tcp service
store := doTest(t, "gitea.yml")
store := doTest(t, "gitea.yml", nil)
assertServiceIPs(t, store, []svc{
{"gitea-ssh", "tcp", "192.168.100.100:20022"},
})
}

func Test_TCPMQTT(t *testing.T) {
// from https://github.com/jittering/traefik-kop/issues/35
store := doTest(t, "mqtt.yml")
store := doTest(t, "mqtt.yml", nil)
assertServiceIPs(t, store, []svc{
{"mqtt", "http", "http://192.168.100.100:9001"},
{"mqtt", "tcp", "192.168.100.100:1883"},
})
}

func Test_helloWorldNoCert(t *testing.T) {
store := doTest(t, "hello-no-cert.yml")
store := doTest(t, "hello-no-cert.yml", nil)

assert.Equal(t, "hello1", store.kv["traefik/http/routers/hello1/service"])
assert.Nil(t, store.kv["traefik/http/routers/hello1/tls/certResolver"])

assertServiceIPs(t, store, []svc{
{"hello1", "http", "http://192.168.100.100:5555"},
})
}

func Test_helloWorldIgnore(t *testing.T) {
store := doTest(t, "hello-ignore.yml", nil)
assert.Nil(t, store.kv["traefik/http/routers/hello1/service"])

// assert.Fail(t, "TODO: check for no cert")
store = doTest(t, "hello-ignore.yml", &Config{Namespace: "foobar"})
assert.Equal(t, "hello1", store.kv["traefik/http/routers/hello1/service"])
assertServiceIPs(t, store, []svc{
{"hello1", "http", "http://192.168.100.100:5555"},
})
}

func Test_samePrefix(t *testing.T) {
Expand Down
Loading

0 comments on commit 58ba8d7

Please sign in to comment.