1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

feat(kube): introduce custom templates [EE-1125] (#5434)

* feat(kube): introduce custom templates

refactor(customtemplates): use build option

chore(deps): upgrade yaml parser

feat(customtemplates): add and edit RC to kube templates

fix(kube): show docker icon

fix(custom-templates): save rc

* fix(kube/templates): route to correct routes
This commit is contained in:
Chaim Lev-Ari 2021-09-02 08:28:51 +03:00 committed by GitHub
parent a176ec5ace
commit e4fe4f9a43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1562 additions and 107 deletions

112
api/kubernetes/yaml.go Normal file
View file

@ -0,0 +1,112 @@
package kubernetes
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type KubeAppLabels struct {
StackID int
Name string
Owner string
Kind string
}
// AddAppLabels adds required labels to "Resource"->metadata->labels.
// It'll add those labels to all Resource (nodes with a kind property exluding a list) it can find in provided yaml.
// Items in the yaml file could either be organised as a list or broken into multi documents.
func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error) {
if bytes.Equal(manifestYaml, []byte("")) {
return manifestYaml, nil
}
docs := make([][]byte, 0)
yamlDecoder := yaml.NewDecoder(bytes.NewReader(manifestYaml))
for {
m := make(map[string]interface{})
err := yamlDecoder.Decode(&m)
// if decoded document is empty
if m == nil {
continue
}
// if there are no more documents in the file
if errors.Is(err, io.EOF) {
break
}
addResourceLabels(m, appLabels)
var out bytes.Buffer
yamlEncoder := yaml.NewEncoder(&out)
yamlEncoder.SetIndent(2)
if err := yamlEncoder.Encode(m); err != nil {
return nil, errors.Wrap(err, "failed to marshal yaml manifest")
}
docs = append(docs, out.Bytes())
}
return bytes.Join(docs, []byte("---\n")), nil
}
func addResourceLabels(yamlDoc interface{}, appLabels KubeAppLabels) {
m, ok := yamlDoc.(map[string]interface{})
if !ok {
return
}
kind, ok := m["kind"]
if ok && !strings.EqualFold(kind.(string), "list") {
addLabels(m, appLabels)
return
}
for _, v := range m {
switch v.(type) {
case map[string]interface{}:
addResourceLabels(v, appLabels)
case []interface{}:
for _, item := range v.([]interface{}) {
addResourceLabels(item, appLabels)
}
}
}
}
func addLabels(obj map[string]interface{}, appLabels KubeAppLabels) {
metadata := make(map[string]interface{})
if m, ok := obj["metadata"]; ok {
metadata = m.(map[string]interface{})
}
labels := make(map[string]string)
if l, ok := metadata["labels"]; ok {
for k, v := range l.(map[string]interface{}) {
labels[k] = fmt.Sprintf("%v", v)
}
}
name := appLabels.Name
if appLabels.Name == "" {
if n, ok := metadata["name"]; ok {
name = n.(string)
}
}
labels["io.portainer.kubernetes.application.stackid"] = strconv.Itoa(appLabels.StackID)
labels["io.portainer.kubernetes.application.name"] = name
labels["io.portainer.kubernetes.application.owner"] = appLabels.Owner
labels["io.portainer.kubernetes.application.kind"] = appLabels.Kind
metadata["labels"] = labels
obj["metadata"] = metadata
}

493
api/kubernetes/yaml_test.go Normal file
View file

@ -0,0 +1,493 @@
package kubernetes
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_AddAppLabels(t *testing.T) {
tests := []struct {
name string
input string
wantOutput string
}{
{
name: "single deployment without labels",
input: `apiVersion: apps/v1
kind: Deployment
metadata:
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
`,
wantOutput: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
`,
},
{
name: "single deployment with existing labels",
input: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
foo: bar
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
`,
wantOutput: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
foo: bar
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
`,
},
{
name: "complex kompose output",
input: `apiVersion: v1
items:
- apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
selector:
io.kompose.service: web
status:
loadBalancer: {}
- apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
io.kompose.service: redis
name: redis
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: redis
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
io.kompose.service: redis
status: {}
- apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
name: web
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: web
strategy:
type: Recreate
template:
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
status: {}
kind: List
metadata: {}
`,
wantOutput: `apiVersion: v1
items:
- apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
selector:
io.kompose.service: web
status:
loadBalancer: {}
- apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
io.kompose.service: redis
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: redis
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: redis
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
io.kompose.service: redis
status: {}
- apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: web
strategy:
type: Recreate
template:
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
status: {}
kind: List
metadata: {}
`,
},
{
name: "multiple items separated by ---",
input: `apiVersion: apps/v1
kind: Deployment
metadata:
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
---
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
selector:
io.kompose.service: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
foo: bar
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
`,
wantOutput: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
---
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
selector:
io.kompose.service: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
foo: bar
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
replicas: 3
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
`,
},
{
name: "empty",
input: "",
wantOutput: "",
},
{
name: "no only deployments",
input: `apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
selector:
io.kompose.service: web
`,
wantOutput: `apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: web
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
selector:
io.kompose.service: web
`,
},
}
labels := KubeAppLabels{
StackID: 123,
Name: "best-name",
Owner: "best-owner",
Kind: "git",
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := AddAppLabels([]byte(tt.input), labels)
assert.NoError(t, err)
assert.Equal(t, tt.wantOutput, string(result))
})
}
}
func Test_AddAppLabels_PickingName_WhenLabelNameIsEmpty(t *testing.T) {
labels := KubeAppLabels{
StackID: 123,
Owner: "best-owner",
Kind: "git",
}
input := `apiVersion: v1
kind: Service
metadata:
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
expected := `apiVersion: v1
kind: Service
metadata:
labels:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: web
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
result, err := AddAppLabels([]byte(input), labels)
assert.NoError(t, err)
assert.Equal(t, expected, string(result))
}
func Test_AddAppLabels_PickingName_WhenLabelAndMetadataNameAreEmpty(t *testing.T) {
labels := KubeAppLabels{
StackID: 123,
Owner: "best-owner",
Kind: "git",
}
input := `apiVersion: v1
kind: Service
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
expected := `apiVersion: v1
kind: Service
metadata:
labels:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: ""
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
result, err := AddAppLabels([]byte(input), labels)
assert.NoError(t, err)
assert.Equal(t, expected, string(result))
}