Skip to content

Commit ebaaf6c

Browse files
committed
Initial revision
Signed-off-by: Markus Blaschke <[email protected]>
0 parents  commit ebaaf6c

20 files changed

+1180
-0
lines changed

.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/test
2+
/vendor
3+
/kube-pool-manager

.editorconfig

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# EditorConfig is awesome: http://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
charset = utf-8
6+
trim_trailing_whitespace = true
7+
8+
[*]
9+
end_of_line = lf
10+
insert_final_newline = true
11+
indent_style = space
12+
indent_size = 4
13+
14+
[Makefile]
15+
indent_style = tab
16+
17+
[*.yml]
18+
indent_size = 2
19+
20+
[*.yaml]
21+
indent_size = 2
22+
23+
[*.conf]
24+
indent_size = 2
25+
26+
[*.go]
27+
indent_style = tab
28+
indent_size = 4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/vendor
2+
/kube-pool-manager
3+
*.exe

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM golang:1.14 as build
2+
3+
WORKDIR /go/src/github.com/webdevops/kube-pool-manager
4+
5+
# Get deps (cached)
6+
COPY ./go.mod /go/src/github.com/webdevops/kube-pool-manager
7+
COPY ./go.sum /go/src/github.com/webdevops/kube-pool-manager
8+
RUN go mod download
9+
10+
# Compile
11+
COPY ./ /go/src/github.com/webdevops/kube-pool-manager
12+
RUN make lint
13+
RUN make build
14+
RUN ./kube-pool-manager --help
15+
16+
#############################################
17+
# FINAL IMAGE
18+
#############################################
19+
FROM gcr.io/distroless/base
20+
ENV LOG_JSON=1
21+
COPY --from=build /go/src/github.com/webdevops/kube-pool-manager/kube-pool-manager /
22+
USER 1000
23+
ENTRYPOINT ["/kube-pool-manager"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2018 WebDevOps
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.PHONY: all build clean image check vendor dependencies
2+
3+
NAME := kube-pool-manager
4+
GIT_TAG := $(shell git describe --dirty --tags --always)
5+
GIT_COMMIT := $(shell git rev-parse --short HEAD)
6+
LDFLAGS := -X "main.gitTag=$(GIT_TAG)" -X "main.gitCommit=$(GIT_COMMIT)" -extldflags "-static"
7+
8+
PKGS := $(shell go list ./... | grep -v -E '/vendor/|/test')
9+
FIRST_GOPATH := $(firstword $(subst :, ,$(shell go env GOPATH)))
10+
GOLANGCI_LINT_BIN := $(FIRST_GOPATH)/bin/golangci-lint
11+
12+
13+
all: build
14+
15+
clean:
16+
git clean -Xfd .
17+
18+
build:
19+
CGO_ENABLED=0 go build -a -ldflags '$(LDFLAGS)' -o $(NAME) .
20+
21+
vendor:
22+
go mod tidy
23+
go mod vendor
24+
go mod verify
25+
26+
image: build
27+
docker build -t $(NAME):$(TAG) .
28+
29+
.PHONY: lint
30+
lint: $(GOLANGCI_LINT_BIN)
31+
# megacheck fails to respect build flags, causing compilation failure during linting.
32+
# instead, use the unused, gosimple, and staticcheck linters directly
33+
$(GOLANGCI_LINT_BIN) run -D megacheck -E unused,gosimple,staticcheck --timeout=10m
34+
35+
dependencies: $(GOLANGCI_LINT_BIN)
36+
37+
$(GOLANGCI_LINT_BIN):
38+
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(FIRST_GOPATH)/bin v1.23.8
39+

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Kube pool manager
2+
=================
3+
4+
[![license](https://img.shields.io/github/license/webdevops/kube-pool-manager.svg)](https://github.com/webdevops/kube-pool-manager/blob/master/LICENSE)
5+
[![Docker](https://img.shields.io/docker/cloud/automated/webdevops/kube-pool-manager)](https://hub.docker.com/r/webdevops/kube-pool-manager/)
6+
[![Docker Build Status](https://img.shields.io/docker/cloud/build/webdevops/kube-pool-manager)](https://hub.docker.com/r/webdevops/kube-pool-manager/)
7+
8+
Manager for Kubernetes pool, automatic applies configuration (annotations, labels, configSource, role) to kubernetes nodes based on any node spec.
9+
10+
Supports JSON path, value and regexp matching.
11+
12+
Sets following settings on nodes if matched:
13+
- node role
14+
- node labels
15+
- node annotations
16+
- node [configSource](https://kubernetes.io/docs/tasks/administer-cluster/reconfigure-kubelet/)
17+
18+
Configuration
19+
-------------
20+
21+
```
22+
Usage:
23+
kube-pool-manager [OPTIONS]
24+
25+
Application Options:
26+
--debug debug mode [$DEBUG]
27+
-v, --verbose verbose mode [$VERBOSE]
28+
--log.json Switch log output to json format [$LOG_JSON]
29+
--dry-run Dry run (do not apply to nodes) [$DRY_RUN]
30+
--config= Config path [$CONFIG]
31+
--bind= Server address (default: :8080) [$SERVER_BIND]
32+
33+
Help Options:
34+
-h, --help Show this help message
35+
```
36+
37+
see [example.yaml](/example.yaml) for configuration file
38+
39+
Metrics
40+
-------
41+
42+
(see `:8080/metrics`)
43+
44+
| Metric | Description |
45+
|:-------------------------------|:------------------------------------------------|
46+
| `poolmanager_node_applied` | Status if node config was applied |
47+
48+
Kubernetes deployment
49+
---------------------
50+
51+
see [deployment](/deployment)

config/config.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
log "github.com/sirupsen/logrus"
6+
"github.com/webdevops/kube-pool-manager/k8s"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/client-go/util/jsonpath"
9+
"regexp"
10+
"strings"
11+
)
12+
13+
type (
14+
Config struct {
15+
Pools []PoolConfig `yaml:"pools"`
16+
}
17+
18+
PoolConfig struct {
19+
Name string `yaml:"pool"`
20+
Selector []PoolConfigSelector `yaml:"selector"`
21+
Node PoolConfigNode `yaml:"node"`
22+
}
23+
24+
PoolConfigSelector struct {
25+
Path string `yaml:"path"`
26+
Match *string `yaml:"match"`
27+
Regexp *string `yaml:"regexp"`
28+
regexp *regexp.Regexp
29+
}
30+
31+
PoolConfigNode struct {
32+
Role *string `yaml:"role"`
33+
ConfigSource *PoolConfigNodeConfigSource `yaml:"configSource"`
34+
Labels *map[string]string `yaml:"labels"`
35+
Annotations *map[string]string `yaml:"annotations"`
36+
}
37+
38+
PoolConfigNodeConfigSource struct {
39+
ConfigMap struct {
40+
Name string `yaml:"name" json:"name"`
41+
Namespace string `yaml:"namespace" json:"namespace"`
42+
KubeletConfigKey string `yaml:"kubeletConfigKey" json:"kubeletConfigKey"`
43+
} `yaml:"configMap" json:"configMap"`
44+
}
45+
)
46+
47+
func (p *PoolConfig) IsMatchingNode(node *corev1.Node) (bool, error) {
48+
for num, selector := range p.Selector {
49+
// auto compile regexp
50+
if selector.Regexp != nil {
51+
p.Selector[num].regexp = regexp.MustCompile(*selector.Regexp)
52+
selector.regexp = p.Selector[num].regexp
53+
}
54+
55+
jpath := jsonpath.New(p.Name)
56+
jpath.AllowMissingKeys(true)
57+
if err := jpath.Parse(selector.Path); err != nil {
58+
return false, err
59+
}
60+
61+
values, err := jpath.FindResults(node)
62+
if err != nil {
63+
return false, err
64+
}
65+
66+
if len(values) == 1 && len(values[0]) == 1 {
67+
val := values[0][0].String()
68+
selectorMatches := false
69+
70+
// compare value
71+
if selector.Match != nil {
72+
if strings.Compare(val, *selector.Match) == 0 {
73+
selectorMatches = true
74+
} else {
75+
log.Tracef("Node \"%s\": path \"%s\" with value \"%s\" is not matching value \"%s\"", node.Name, selector.Path, val, *selector.Match)
76+
}
77+
}
78+
79+
// regexp
80+
if selector.regexp != nil {
81+
if selector.regexp.MatchString(val) {
82+
selectorMatches = true
83+
} else {
84+
log.Tracef("Node \"%s\": path \"%s\" with value \"%s\" is not matching regexp \"%s\"", node.Name, selector.Path, val, *selector.Regexp)
85+
}
86+
}
87+
88+
if !selectorMatches {
89+
return false, nil
90+
}
91+
} else {
92+
// not found -> not matching
93+
log.Tracef("Node \"%s\": path \"%s\" not found", node.Name, selector.Path)
94+
return false, nil
95+
}
96+
}
97+
98+
return true, nil
99+
}
100+
101+
func (p *PoolConfig) CreateJsonPatchSet() (patches []k8s.JsonPatch) {
102+
patches = []k8s.JsonPatch{}
103+
104+
if p.Node.Role != nil {
105+
name := "kubernetes.io/role"
106+
patches = append(patches, k8s.JsonPatchString{
107+
Op: "replace",
108+
Path: fmt.Sprintf("/metadata/labels/%s", k8s.PatchPathEsacpe(name)),
109+
Value: *p.Node.Role,
110+
})
111+
}
112+
113+
if p.Node.ConfigSource != nil {
114+
patches = append(patches, k8s.JsonPatchObject{
115+
Op: "replace",
116+
Path: "/spec/configSource",
117+
Value: *p.Node.ConfigSource,
118+
})
119+
}
120+
121+
if p.Node.Labels != nil {
122+
for name, value := range *p.Node.Labels {
123+
patches = append(patches, k8s.JsonPatchString{
124+
Op: "replace",
125+
Path: fmt.Sprintf("/metadata/labels/%s", k8s.PatchPathEsacpe(name)),
126+
Value: value,
127+
})
128+
}
129+
}
130+
131+
if p.Node.Annotations != nil {
132+
for name, value := range *p.Node.Annotations {
133+
patches = append(patches, k8s.JsonPatchString{
134+
Op: "replace",
135+
Path: fmt.Sprintf("/metadata/annotations/%s", k8s.PatchPathEsacpe(name)),
136+
Value: value,
137+
})
138+
}
139+
}
140+
141+
return patches
142+
}

config/config_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package config
2+
3+
import (
4+
corev1 "k8s.io/api/core/v1"
5+
"testing"
6+
)
7+
8+
func stringPtr(val string) *string {
9+
return &val
10+
}
11+
12+
func buildNode() *corev1.Node {
13+
node := corev1.Node{}
14+
node.Spec.ProviderID = "azure:///subscriptions/d86bcf13-ddf7-45ea-82f1-6f656767a318/resourceGroups/mc_k8s_mblaschke_westeurope/providers/Microsoft.Compute/virtualMachineScaleSets/aks-agents-35471996-vmss/virtualMachines/30"
15+
node.ObjectMeta.Annotations = map[string]string{
16+
"node.kubernetes.io/foobar": "barfoo",
17+
}
18+
node.ObjectMeta.Labels = map[string]string{
19+
"node.kubernetes.io/role": "worker",
20+
}
21+
22+
return &node
23+
}
24+
25+
func Test_NodeMatcher(t *testing.T) {
26+
node := buildNode()
27+
28+
pool := PoolConfig{
29+
Name: "testing",
30+
Selector: []PoolConfigSelector{
31+
{
32+
Path: "{.spec.providerID}",
33+
Match: stringPtr("azure:///subscriptions/d86bcf13-ddf7-45ea-82f1-6f656767a318/resourceGroups/mc_k8s_mblaschke_westeurope/providers/Microsoft.Compute/virtualMachineScaleSets/aks-agents-35471996-vmss/virtualMachines/30"),
34+
},
35+
},
36+
}
37+
matching, err := pool.IsMatchingNode(node)
38+
if err != nil {
39+
t.Errorf("Unexpected error: %v", err)
40+
}
41+
if !matching {
42+
t.Error("Expected not matching, but matching node")
43+
}
44+
45+
pool.Selector[0].Match = stringPtr("azure:///subscriptions/d86bcf13-ddf7-45ea-82f1-6f656767a318/resourceGroups/mc_k8s_mblaschke_westeurope/providers/Microsoft.Compute/virtualMachineScaleSets/aks-agents-35471996-vmss/virtualMachines/31")
46+
matching, err = pool.IsMatchingNode(node)
47+
if err != nil {
48+
t.Errorf("Unexpected error: %v", err)
49+
}
50+
if matching {
51+
t.Error("Expected matching, but not matching node")
52+
}
53+
}
54+
55+
func Test_NodeRegexp(t *testing.T) {
56+
node := buildNode()
57+
58+
pool := PoolConfig{
59+
Name: "testing",
60+
Selector: []PoolConfigSelector{
61+
{
62+
Path: "{.spec.providerID}",
63+
Regexp: stringPtr("^.+/resourceGroups/mc_k8s_mblaschke_westeurope/.+$"),
64+
},
65+
},
66+
}
67+
matching, err := pool.IsMatchingNode(node)
68+
if err != nil {
69+
t.Errorf("Unexpected error: %v", err)
70+
}
71+
if !matching {
72+
t.Error("Expected not matching, but matching node")
73+
}
74+
}

0 commit comments

Comments
 (0)