mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 14:26:23 -04:00
Move cloud metadata packages and kubernetes resources marshaling to internal
Decouples cloud provider metadata packages from kubernetes related code Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
parent
89e3acf6a1
commit
26e9c67a00
81 changed files with 169 additions and 145 deletions
18
internal/kubernetes/configmaps.go
Normal file
18
internal/kubernetes/configmaps.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
k8s "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// ConfigMaps represent a list of k8s ConfigMap.
|
||||
type ConfigMaps []*k8s.ConfigMap
|
||||
|
||||
// Marshal marshals config maps into multiple YAML documents.
|
||||
func (s ConfigMaps) Marshal() ([]byte, error) {
|
||||
objects := make([]runtime.Object, len(s))
|
||||
for i := range s {
|
||||
objects[i] = s[i]
|
||||
}
|
||||
return MarshalK8SResourcesList(objects)
|
||||
}
|
49
internal/kubernetes/configmaps_test.go
Normal file
49
internal/kubernetes/configmaps_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestConfigMaps(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
configMaps := ConfigMaps{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{"key": "value1"},
|
||||
},
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{"key": "value2"},
|
||||
},
|
||||
}
|
||||
data, err := configMaps.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(`apiVersion: v1
|
||||
data:
|
||||
key: value1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: value2
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`, string(data))
|
||||
}
|
149
internal/kubernetes/marshal.go
Normal file
149
internal/kubernetes/marshal.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
// Marshaler is used by all k8s resources that can be marshaled to YAML.
|
||||
type Marshaler interface {
|
||||
Marshal() ([]byte, error)
|
||||
}
|
||||
|
||||
// MarshalK8SResources marshals every field of a struct into a k8s resource YAML.
|
||||
func MarshalK8SResources(resources any) ([]byte, error) {
|
||||
if resources == nil {
|
||||
return nil, errors.New("marshal on nil called")
|
||||
}
|
||||
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil)
|
||||
var buf bytes.Buffer
|
||||
|
||||
// reflect over struct containing fields that are k8s resources
|
||||
value := reflect.ValueOf(resources)
|
||||
if value.Kind() != reflect.Ptr && value.Kind() != reflect.Interface {
|
||||
return nil, errors.New("marshal on non-pointer called")
|
||||
}
|
||||
elem := value.Elem()
|
||||
if elem.Kind() == reflect.Struct {
|
||||
// iterate over all struct fields
|
||||
for i := 0; i < elem.NumField(); i++ {
|
||||
field := elem.Field(i)
|
||||
var inter any
|
||||
// check if value can be converted to interface
|
||||
if field.CanInterface() {
|
||||
inter = field.Addr().Interface()
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// convert field interface to runtime.Object
|
||||
obj, ok := inter.(runtime.Object)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
// separate YAML documents
|
||||
buf.Write([]byte("---\n"))
|
||||
}
|
||||
// serialize k8s resource
|
||||
if err := serializer.Encode(obj, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalK8SResources takes YAML and converts it into a k8s resources struct.
|
||||
func UnmarshalK8SResources(data []byte, into any) error {
|
||||
if into == nil {
|
||||
return errors.New("unmarshal on nil called")
|
||||
}
|
||||
// reflect over struct containing fields that are k8s resources
|
||||
value := reflect.ValueOf(into).Elem()
|
||||
if value.Kind() != reflect.Struct {
|
||||
return errors.New("can only reflect over struct")
|
||||
}
|
||||
|
||||
decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder()
|
||||
documents, err := splitYAML(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("splitting deployment YAML into multiple documents: %w", err)
|
||||
}
|
||||
if len(documents) != value.NumField() {
|
||||
return fmt.Errorf("expected %v YAML documents, got %v", value.NumField(), len(documents))
|
||||
}
|
||||
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := value.Field(i)
|
||||
var inter any
|
||||
// check if value can be converted to interface
|
||||
if !field.CanInterface() {
|
||||
return fmt.Errorf("cannot use struct field %v as interface", i)
|
||||
}
|
||||
inter = field.Addr().Interface()
|
||||
// convert field interface to runtime.Object
|
||||
obj, ok := inter.(runtime.Object)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot convert struct field %v as k8s runtime object", i)
|
||||
}
|
||||
|
||||
// decode YAML document into struct field
|
||||
if err := runtime.DecodeInto(decoder, documents[i], obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalK8SResourcesList marshals every element of a slice into a k8s resource YAML.
|
||||
func MarshalK8SResourcesList(resources []runtime.Object) ([]byte, error) {
|
||||
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil)
|
||||
var buf bytes.Buffer
|
||||
|
||||
for i, obj := range resources {
|
||||
if i > 0 {
|
||||
// separate YAML documents
|
||||
buf.Write([]byte("---\n"))
|
||||
}
|
||||
// serialize k8s resource
|
||||
if err := serializer.Encode(obj, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// splitYAML splits a YAML multidoc into a slice of multiple YAML docs.
|
||||
func splitYAML(resources []byte) ([][]byte, error) {
|
||||
dec := yaml.NewDecoder(bytes.NewReader(resources))
|
||||
var res [][]byte
|
||||
for {
|
||||
var value any
|
||||
err := dec.Decode(&value)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueBytes, err := yaml.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, valueBytes)
|
||||
}
|
||||
return res, nil
|
||||
}
|
360
internal/kubernetes/marshal_test.go
Normal file
360
internal/kubernetes/marshal_test.go
Normal file
|
@ -0,0 +1,360 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestMarshalK8SResources(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
resources any
|
||||
wantErr bool
|
||||
wantYAML string
|
||||
}{
|
||||
"ConfigMap as only field can be marshaled": {
|
||||
resources: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Multiple fields are correctly encoded": {
|
||||
resources: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
Secret k8s.Secret
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Secret: k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Non-pointer is detected": {
|
||||
resources: "non-pointer",
|
||||
wantErr: true,
|
||||
},
|
||||
"Nil resource pointer is detected": {
|
||||
resources: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
"Non-pointer field is ignored": {
|
||||
resources: &struct{ String string }{String: "somestring"},
|
||||
},
|
||||
"nil field is ignored": {
|
||||
resources: &struct {
|
||||
ConfigMap *k8s.ConfigMap
|
||||
}{
|
||||
ConfigMap: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
yaml, err := MarshalK8SResources(tc.resources)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.wantYAML, string(yaml))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalK8SResources(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
data string
|
||||
into any
|
||||
wantObj any
|
||||
wantErr bool
|
||||
}{
|
||||
"ConfigMap as only field can be unmarshaled": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{},
|
||||
wantObj: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Multiple fields are correctly unmarshaled": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
Secret k8s.Secret
|
||||
}{},
|
||||
wantObj: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
Secret k8s.Secret
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Secret: k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Mismatching amount of fields is detected": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
"Non-struct pointer is detected": {
|
||||
into: proto.String("test"),
|
||||
wantErr: true,
|
||||
},
|
||||
"Nil into is detected": {
|
||||
into: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
"Invalid yaml is detected": {
|
||||
data: `duplicateKey: value
|
||||
duplicateKey: value`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
"Struct field cannot interface with runtime.Object": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
String string
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
"Struct field mismatch": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
Secret k8s.Secret
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
err := UnmarshalK8SResources([]byte(tc.data), tc.into)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.wantObj, tc.into)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalK8SResourcesList(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
resources []runtime.Object
|
||||
wantErr bool
|
||||
wantYAML string
|
||||
}{
|
||||
"ConfigMap as only element be marshaled": {
|
||||
resources: []runtime.Object{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Multiple fields are correctly encoded": {
|
||||
resources: []runtime.Object{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Nil resource pointer is encodes": {
|
||||
resources: []runtime.Object{nil},
|
||||
wantYAML: "null\n",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
yaml, err := MarshalK8SResourcesList(tc.resources)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.wantYAML, string(yaml))
|
||||
})
|
||||
}
|
||||
}
|
18
internal/kubernetes/secrets.go
Normal file
18
internal/kubernetes/secrets.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
k8s "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// Secrets represent a list of k8s Secret.
|
||||
type Secrets []*k8s.Secret
|
||||
|
||||
// Marshal marshals secrets into multiple YAML documents.
|
||||
func (s Secrets) Marshal() ([]byte, error) {
|
||||
objects := make([]runtime.Object, len(s))
|
||||
for i := range s {
|
||||
objects[i] = s[i]
|
||||
}
|
||||
return MarshalK8SResourcesList(objects)
|
||||
}
|
49
internal/kubernetes/secrets_test.go
Normal file
49
internal/kubernetes/secrets_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
secrets := Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value1")},
|
||||
},
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value2")},
|
||||
},
|
||||
}
|
||||
data, err := secrets.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(`apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWUx
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWUy
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`, string(data))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue