Alexey Odinokov e2c56108ee Nextgen secrets implementation with separation per cluster
1. Extending templater with kyaml functions and creating combined catalogue
to be able to request/update the existing resources.
This is based on 'everything is transformer' concept introduced in kustomize 4.x
That includes gathering all secrets into 1 variable catalogue and
special mechanism to regenerate/merge with manual secrets.

2. Implementing 'catalogue per cluster' approach for secrets.

3. Rearranging secrets so it's possible to use:
pgp (each person may have his own key), age, Hachicorp Vault and etc
and the list of people who can decrypt documents is set in a special file.
Since in some cases there should be a separate list of people who can decrypt
data - this list is set for each cluster (ephemeral and target) separatelly.

Closes: #586
Change-Id: I038f84dd138d5ad4a35f4862c61ff2124c2fd530
2021-09-03 20:46:15 +00:00

635 lines
12 KiB
Go

/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package templater
import (
"bytes"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/kio"
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/yaml"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
)
func TestTemplater(t *testing.T) {
testCases := []struct {
in string
cfg string
expectedOut string
expectedErr string
}{
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
hosts:
- macAddress: 00:aa:bb:cc:dd
name: node-1
- macAddress: 00:aa:bb:cc:ee
name: node-2
template: |
{{ range .hosts -}}
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
name: {{ .name }}
spec:
bootMACAddress: {{ .macAddress }}
{{ end -}}`,
expectedOut: `apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
name: node-1
spec:
bootMACAddress: 00:aa:bb:cc:dd
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
name: node-2
spec:
bootMACAddress: 00:aa:bb:cc:ee
`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
test:
someKey:
anotherKey: value
of:
- toYaml
template: |
{{ toYaml . -}}
`,
expectedOut: `test:
of:
- toYaml
someKey:
anotherKey: value
`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
test:
someKey:
anotherKey: value
of:
- toYaml
template: |
{{- $_ := setItems getItems -}}
{{ toYaml . -}}
`,
expectedOut: `test:
of:
- toYaml
someKey:
anotherKey: value
`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
test:
of:
- badToYamlInput
template: |
{{ toYaml ignorethisbadinput -}}
`,
expectedErr: `template: notImportantHere:1: function "ignorethisbadinput" not defined`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
template: |
{{ end }`,
expectedErr: "template: notImportantHere:1: unexpected \"}\" in end",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
template: |
touint32: {{ toUint32 10 -}}
`,
expectedOut: `touint32: 10
`,
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
regex: "^[a-z]{5,10}$"
nomatchregex: "^[a-z]{0,4}$"
limit: 10
template: |
truepassword: {{ regexMatch .regex (regexGen .regex (.limit|int)) }}
falsepassword: {{ regexMatch .nomatchregex (regexGen .regex (.limit|int)) }}
`,
expectedOut: `truepassword: true
falsepassword: false
`,
}, {
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
name: test
regex: "^[a-z]{5,10}$"
limit: 0
template: |
password: {{ regexGen .regex (.limit|int) }}
`,
expectedErr: "template: notImportantHere:1:13: executing \"notImportantHere\" at " +
"<regexGen .regex (.limit | int)>: error calling regexGen: " +
"Limit cannot be less than or equal to 0",
},
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
name: test
regex: "^[a-z"
limit: 10
template: |
password: {{ regexGen .regex (.limit|int) }}
`,
expectedErr: "template: notImportantHere:1:13: executing \"notImportantHere\" " +
"at <regexGen .regex (.limit | int)>: error calling " +
"regexGen: error parsing regexp: missing closing ]: `[a-z`",
},
// transformer tests
{
in: `
apiVersion: v1
kind: ConfigMap
metadata:
name: map1
`,
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
annotationTransf: |
kind: AnnotationSetter
key: test-annotation
value: %s
template: |
{{- $_ := KPipe getItems (list (KYFilter (list (YFilter (printf .annotationTransf "testenvvalue"))))) -}}
`,
expectedOut: `apiVersion: v1
kind: ConfigMap
metadata:
name: map1
annotations:
test-annotation: 'testenvvalue'
`,
},
{
in: `
apiVersion: v1
kind: ConfigMap
metadata:
name: map1
data:
value: value1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: map2
data:
value: value2
`,
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
map1grep: |
kind: GrepFilter
path:
- metadata
- name
value: ^map1$
pathGet1: |
kind: PathGetter
path:
- data
- value
map2grep: |
kind: GrepFilter
path:
- metadata
- name
value: ^map2$
map2PathGet: |
kind: PathGetter
path:
- data
fieldSet: |
kind: FieldSetter
name: value
stringValue: %s
template: |
{{- $map1 := KPipe getItems (list (KFilter .map1grep)) -}}
{{- $map1value := YValue (YPipe (index $map1 0) (list (YFilter .pathGet1))) -}}
{{- $kyflt := KYFilter (list (YFilter .map2PathGet) (YFilter (printf .fieldSet $map1value))) -}}
{{- $_ := KPipe getItems (list (KFilter .map2grep) $kyflt) -}}
`,
expectedOut: `apiVersion: v1
kind: ConfigMap
metadata:
name: map1
data:
value: value1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: map2
data:
value: value1
`,
},
{
in: `
apiVersion: v1
kind: ConfigMap
metadata:
name: map1
annotations:
test-annotation: x
data:
value: value1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: map2
data:
value: value2
`,
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
grep: |
kind: GrepFilter
path:
- metadata
- annotations
- test-annotation
value: ^x$
invertMatch: true
template: |
{{- $_ := setItems (KPipe getItems (list (KFilter .grep))) -}}
`,
expectedOut: `apiVersion: v1
kind: ConfigMap
metadata:
name: map2
data:
value: value2
`,
},
{
in: `
apiVersion: v1
kind: ConfigMap
metadata:
name: map1
annotations:
test-annotation: x
data:
value: value1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: map2
data:
value: value2
`,
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
grep: |
kind: GrepFilter
path:
- metadata
- annotations
- test-annotation
value: ^x$
invertMatch: true
template: |
{{- $_ := setItems (KOneFilter getItems .grep) -}}
`,
expectedOut: `apiVersion: v1
kind: ConfigMap
metadata:
name: map2
data:
value: value2
`,
},
{
in: ``,
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
template: |
{{ define "tmplx" }}
{{- $name:= . -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ $name }}
{{ end }}
{{ include "tmplx" "cfg1" }}
---
{{ include "tmplx" "cfg2" }}
`,
expectedOut: `apiVersion: v1
kind: ConfigMap
metadata:
name: cfg1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cfg2
`,
},
{
in: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: libModule
template: |
{{/* grepTpl returns yaml that can be used to built KFilter that will
filter with grep */}}
{{- define "grepTpl" -}}
kind: GrepFilter
path: {{ index . 0 }}
value: {{ index . 1 }}
{{ if gt (len .) 2}}
invertMatch: {{ index . 2 }}
{{ end }}
{{- end -}}
{{/* test function */}}
{{ define "fnFromModule" }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ index . 0 }}
{{ end }}
`,
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
template: |
{{/* remove all modules (they already imported) */}}
{{- $_ := setItems (KOneFilter getItems (include "grepTpl" (list "[\"kind\"]" "^Templater$" "true"))) -}}
{{/* call fn from imported module */}}
{{ include "fnFromModule" (list "cfg1") }}
`,
expectedOut: `apiVersion: v1
kind: ConfigMap
metadata:
name: cfg1
`,
},
}
for _, tc := range testCases {
cfg := make(map[string]interface{})
err := yaml.Unmarshal([]byte(tc.cfg), &cfg)
require.NoError(t, err)
plugin, err := New(cfg)
require.NoError(t, err)
nodesIn, err := (&kio.ByteReader{Reader: bytes.NewBufferString(tc.in)}).Read()
require.NoError(t, err)
buf := &bytes.Buffer{}
nodes, err := plugin.Filter(nodesIn)
if tc.expectedErr != "" {
assert.EqualError(t, err, tc.expectedErr)
continue
}
require.NoError(t, err)
err = kio.ByteWriter{Writer: buf}.Write(nodes)
require.NoError(t, err)
assert.Equal(t, tc.expectedOut, buf.String())
}
}
func TestGenSignedCertEx(t *testing.T) {
testCases := []struct {
cfg string
expectedSubject pkix.Name
}{
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
name: test
regex: "^[a-z]{5,10}$"
limit: 0
template: |
{{- $targetClusterCa:=genCAEx "Kubernetes API" 3650 }}
{{- $targetKubeconfigCert:= genSignedCertEx "/CN=admin/O=system:masters" nil nil 365 $targetClusterCa }}
cert: {{ $targetKubeconfigCert.Cert|b64enc|quote }}
`,
expectedSubject: pkix.Name{
CommonName: `admin`,
Organization: []string{
`system:masters`,
},
},
},
}
for _, tc := range testCases {
cfg := make(map[string]interface{})
err := yaml.Unmarshal([]byte(tc.cfg), &cfg)
require.NoError(t, err)
plugin, err := New(cfg)
require.NoError(t, err)
buf := &bytes.Buffer{}
nodes, err := plugin.Filter(nil)
require.NoError(t, err)
err = kio.ByteWriter{Writer: buf}.Write(nodes)
require.NoError(t, err)
res := make(map[string]string)
err = yaml.Unmarshal(buf.Bytes(), &res)
require.NoError(t, err)
key, err := base64.StdEncoding.DecodeString(res["cert"])
require.NoError(t, err)
der, _ := pem.Decode(key)
if der == nil {
t.Errorf("failed to find PEM block")
return
}
cert, err := x509.ParseCertificate(der.Bytes)
if err != nil {
t.Errorf("failed to parse: %s", err)
return
}
cert.Subject.Names = nil
assert.Equal(t, tc.expectedSubject, cert.Subject)
}
}
func TestGetRNodes(t *testing.T) {
//Prepare test data A, B, C,
//var x []*yaml.RNode
rnode1, err := kyaml.Parse(`x: y`)
require.NoError(t, err)
rnode2, err := kyaml.Parse(`z: "a"`)
require.NoError(t, err)
testA := []*kyaml.RNode{
rnode1,
rnode2,
}
testB := []interface{}{
rnode1,
rnode2,
}
testCases := []struct {
rnodesarr interface{}
expectedOut string
expectedErr bool
}{
{
rnodesarr: nil,
expectedErr: true,
},
{
rnodesarr: testA,
expectedOut: `
x: y
---
z: "a"
`,
},
{
rnodesarr: testB,
expectedOut: `
x: y
---
z: "a"
`,
},
}
for i, tc := range testCases {
nodes, err := getRNodes(tc.rnodesarr)
if tc.expectedErr && err != nil {
continue
}
if tc.expectedErr {
t.Errorf("expected error, but hasn't got it for the case %d", i)
continue
}
if err != nil {
t.Errorf("got unexpected error: %v", err)
continue
}
// convert to string and compare with expected
out := &bytes.Buffer{}
err = kio.ByteWriter{Writer: out}.Write(nodes)
require.NoError(t, err)
assert.Equal(t, tc.expectedOut[1:], out.String())
}
}
func TestDebug(t *testing.T) {
i := 0
os.Setenv("DEBUG_TEMPLATER", "false")
debug(func() { i = 1 })
assert.Equal(t, 0, i)
os.Setenv("DEBUG_TEMPLATER", "true")
debug(func() { i = 1 })
assert.Equal(t, 1, i)
}