diff --git a/pkg/client/kubernetes/builder_test.go b/pkg/client/kubernetes/builder_test.go index bad89d0..181c202 100644 --- a/pkg/client/kubernetes/builder_test.go +++ b/pkg/client/kubernetes/builder_test.go @@ -1,16 +1,11 @@ package kubernetes import ( - "bytes" - "log" "testing" "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/printers" - "k8s.io/client-go/kubernetes/scheme" ) func TestBuildResources(t *testing.T) { @@ -159,8 +154,8 @@ status: actualService.TypeMeta = metav1.TypeMeta{Kind: "Service", APIVersion: "v1"} assert.NoError(t, err) - assert.YAMLEqf(t, expectedDeployment, objectToYaml(actualDeployment), "unexpected deployment") - assert.YAMLEqf(t, expectedService, objectToYaml(actualService), "unexpected service") + assert.YAMLEqf(t, expectedDeployment, ObjectToYaml(actualDeployment), "unexpected deployment") + assert.YAMLEqf(t, expectedService, ObjectToYaml(actualService), "unexpected service") } func TestBuildJob(t *testing.T) { @@ -229,25 +224,5 @@ status: {} // fix model actualJob.TypeMeta = metav1.TypeMeta{Kind: "Job", APIVersion: "batch/v1"} - assert.YAMLEqf(t, expectedJob, objectToYaml(actualJob), "unexpected job") -} - -func objectToYaml(object runtime.Object) string { - buffer := new(bytes.Buffer) - printer := printers.YAMLPrinter{} - if err := printer.PrintObj(object, buffer); err != nil { - log.Fatalf("objectToYaml: %#v\n", err) - } - return buffer.String() -} - -// https://github.com/kubernetes/client-go/issues/193 -// https://medium.com/@harshjniitr/reading-and-writing-k8s-resource-as-yaml-in-golang-81dc8c7ea800 -func yamlToDeployment(data string) *appsv1.Deployment { - decoder := scheme.Codecs.UniversalDeserializer().Decode - object, _, err := decoder([]byte(data), nil, nil) - if err != nil { - log.Fatalf("yamlToDeployment: %#v\n", err) - } - return object.(*appsv1.Deployment) + assert.YAMLEqf(t, expectedJob, ObjectToYaml(actualJob), "unexpected job") } diff --git a/pkg/client/kubernetes/util.go b/pkg/client/kubernetes/util.go new file mode 100644 index 0000000..d8a6831 --- /dev/null +++ b/pkg/client/kubernetes/util.go @@ -0,0 +1,31 @@ +package kubernetes + +import ( + "bytes" + "log" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/client-go/kubernetes/scheme" +) + +func ObjectToYaml(object runtime.Object) string { + buffer := new(bytes.Buffer) + printer := printers.YAMLPrinter{} + if err := printer.PrintObj(object, buffer); err != nil { + log.Fatalf("ObjectToYaml: %#v\n", err) + } + return buffer.String() +} + +// https://github.com/kubernetes/client-go/issues/193 +// https://medium.com/@harshjniitr/reading-and-writing-k8s-resource-as-yaml-in-golang-81dc8c7ea800 +func YamlToDeployment(data string) *appsv1.Deployment { + decoder := scheme.Codecs.UniversalDeserializer().Decode + object, _, err := decoder([]byte(data), nil, nil) + if err != nil { + log.Fatalf("YamlToDeployment: %#v\n", err) + } + return object.(*appsv1.Deployment) +} diff --git a/pkg/common/kubernetes/builder.go b/pkg/common/kubernetes/builder.go new file mode 100644 index 0000000..02df6e2 --- /dev/null +++ b/pkg/common/kubernetes/builder.go @@ -0,0 +1,86 @@ +package kubernetes + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + commonModel "github.com/hckops/hckctl/pkg/common/model" +) + +const ( + secretBasePath = "/secrets" + sidecarVpnTunnelVolume = "tun-device-volume" + sidecarVpnTunnelPath = "/dev/net/tun" + sidecarVpnSecretVolume = "sidecar-vpn-volume" + sidecarVpnSecretPath = "/secrets/openvpn/client.ovpn" + sidecarVpnSecretKey = "openvpn-config" +) + +func buildSidecarVpnSecretName(containerName string) string { + return fmt.Sprintf("%s-sidecar-vpn-secret", containerName) +} + +func buildSidecarVpnSecret(namespace, containerName, secretValue string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: buildSidecarVpnSecretName(containerName), + Namespace: namespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{sidecarVpnSecretKey: []byte(secretValue)}, + } +} + +func buildSidecarVpnContainer() corev1.Container { + return corev1.Container{ + Name: "sidecar-vpn", + Image: commonModel.SidecarVpnImageName, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + {Name: "OPENVPN_CONFIG", Value: sidecarVpnSecretPath}, + }, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: sidecarVpnTunnelVolume, + MountPath: sidecarVpnTunnelPath, + ReadOnly: true, + }, + { + Name: sidecarVpnSecretVolume, + MountPath: secretBasePath, + ReadOnly: true, + }, + }, + } +} + +func buildSidecarVpnVolumes(containerName string) []corev1.Volume { + return []corev1.Volume{ + { + Name: sidecarVpnTunnelVolume, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: sidecarVpnTunnelPath, + }, + }, + }, + { + Name: sidecarVpnSecretVolume, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: buildSidecarVpnSecretName(containerName), + Items: []corev1.KeyToPath{ + {Key: sidecarVpnSecretKey, Path: sidecarVpnSecretPath}, + }, + }, + }, + }, + } +} diff --git a/pkg/common/kubernetes/builder_test.go b/pkg/common/kubernetes/builder_test.go new file mode 100644 index 0000000..8d5027b --- /dev/null +++ b/pkg/common/kubernetes/builder_test.go @@ -0,0 +1,89 @@ +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/hckops/hckctl/pkg/client/kubernetes" + "github.com/hckops/hckctl/pkg/util" +) + +func TestBuildSidecarVpnSecret(t *testing.T) { + + expected := ` +apiVersion: v1 +data: + openvpn-config: bXktdmFsdWU= +kind: Secret +metadata: + creationTimestamp: null + name: my-container-name-sidecar-vpn-secret + namespace: my-namespace +type: Opaque +` + + actual := buildSidecarVpnSecret("my-namespace", "my-container-name", "my-value") + // fix model + actual.TypeMeta = metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"} + + assert.YAMLEqf(t, expected, kubernetes.ObjectToYaml(actual), "unexpected secret") + + decoded, ok := util.Base64Decode("bXktdmFsdWU=") + assert.True(t, ok) + assert.Equal(t, "my-value", decoded) +} + +func TestBuildSidecarVpnPod(t *testing.T) { + + expected := ` +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null +spec: + containers: + - env: + - name: OPENVPN_CONFIG + value: /secrets/openvpn/client.ovpn + image: hckops/alpine-openvpn:latest + imagePullPolicy: IfNotPresent + name: sidecar-vpn + resources: {} + securityContext: + capabilities: + add: + - NET_ADMIN + volumeMounts: + - mountPath: /dev/net/tun + name: tun-device-volume + readOnly: true + - mountPath: /secrets + name: sidecar-vpn-volume + readOnly: true + volumes: + - hostPath: + path: /dev/net/tun + name: tun-device-volume + - name: sidecar-vpn-volume + secret: + items: + - key: openvpn-config + path: /secrets/openvpn/client.ovpn + secretName: main-container-sidecar-vpn-secret +status: {} +` + + actual := &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{buildSidecarVpnContainer()}, + Volumes: buildSidecarVpnVolumes("main-container"), + }, + } + // fix model + actual.TypeMeta = metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"} + + assert.YAMLEqf(t, expected, kubernetes.ObjectToYaml(actual), "unexpected pod") +} diff --git a/pkg/util/string.go b/pkg/util/string.go index 29d9877..8d022a5 100644 --- a/pkg/util/string.go +++ b/pkg/util/string.go @@ -1,6 +1,7 @@ package util import ( + "encoding/base64" "fmt" "os" "regexp" @@ -98,3 +99,25 @@ func Expand(raw string, inputs map[string]string) (string, error) { return expanded, err } + +func Base64Encode(value string) string { + // alternative with "len" + //encoded := make([]byte, base64.StdEncoding.EncodedLen(len(value))) + //base64.StdEncoding.Encode(encoded, []byte(value)) + //return string(encoded) + + return base64.StdEncoding.EncodeToString([]byte(value)) +} + +func Base64Decode(value string) (string, bool) { + // alternative with "len" + //decoded := make([]byte, base64.StdEncoding.DecodedLen(len(value))) + //count, err := base64.StdEncoding.Decode(decoded, []byte(value)) + //return string(decoded) + + decoded, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", false + } + return string(decoded), true +} diff --git a/pkg/util/string_test.go b/pkg/util/string_test.go index 8f7054a..8fcf39e 100644 --- a/pkg/util/string_test.go +++ b/pkg/util/string_test.go @@ -9,3 +9,13 @@ import ( func TestToLowerKebabCase(t *testing.T) { assert.Equal(t, "hckops-my-test_value-example", ToLowerKebabCase(" hCKops/my-tEst_value$ExamplE\t")) } + +func TestBase64(t *testing.T) { + value := "hello world" + encoded := Base64Encode(value) + assert.Equal(t, "aGVsbG8gd29ybGQ=", encoded) + + decoded, ok := Base64Decode(encoded) + assert.True(t, ok) + assert.Equal(t, value, decoded) +}