Skip to content

Commit

Permalink
Update to use unstructured
Browse files Browse the repository at this point in the history
  • Loading branch information
dramich committed Jul 9, 2019
1 parent 4903549 commit f70d6e1
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 42 deletions.
22 changes: 16 additions & 6 deletions backend/remote-state/kubernetes/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
homedir "github.com/mitchellh/go-homedir"
kubernetes "k8s.io/client-go/kubernetes"
"github.com/mitchellh/go-homedir"
k8sSchema "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
Expand All @@ -28,6 +29,14 @@ this could cause issues connecting to your Kubernetes cluster.
`
)

var (
secretResource = k8sSchema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
}
)

// New creates a new backend for kubernetes remote state.
func New() backend.Backend {
s := &schema.Backend{
Expand Down Expand Up @@ -143,7 +152,7 @@ type Backend struct {
*schema.Backend

// The fields below are set from configure
k8sClient kubernetes.Interface
k8sClient dynamic.ResourceInterface
namespace string
nameSuffix string
}
Expand Down Expand Up @@ -189,13 +198,14 @@ func (b *Backend) configure(ctx context.Context) error {
cfg.BearerToken = v.(string)
}

client, err := kubernetes.NewForConfig(cfg)
client, err := dynamic.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("Failed to configure: %s", err)
}

b.k8sClient = client
b.namespace = data.Get("namespace").(string)
ns := data.Get("namespace").(string)
b.k8sClient = client.Resource(secretResource).Namespace(ns)
b.namespace = ns
b.nameSuffix = data.Get("key").(string)

return nil
Expand Down
7 changes: 4 additions & 3 deletions backend/remote-state/kubernetes/backend_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// Workspaces returns a list of names for the workspaces found in k8s. The default
// workspace is always returned as the first element in the slice.
func (b *Backend) Workspaces() ([]string, error) {
secrets, err := b.k8sClient.CoreV1().Secrets(b.namespace).List(
secrets, err := b.k8sClient.List(
metav1.ListOptions{
LabelSelector: terraState + "=true",
},
Expand All @@ -27,12 +27,13 @@ func (b *Backend) Workspaces() ([]string, error) {
// Use a map so there aren't duplicate workspaces
sMap := make(map[string]struct{})
for _, secret := range secrets.Items {
ws, ok := secret.Labels[terraWorkspace]
sl := secret.GetLabels()
ws, ok := sl[terraWorkspace]
if !ok {
continue
}

key, ok := secret.Labels[terraKey]
key, ok := sl[terraKey]
if !ok {
continue
}
Expand Down
9 changes: 5 additions & 4 deletions backend/remote-state/kubernetes/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func cleanupK8sSecrets(t *testing.T) {

b := b1.(*Backend)

sClient := b.k8sClient.CoreV1().Secrets(namespace)
sClient := b.k8sClient

// Get state secrets based off the terraState label
opts := metav1.ListOptions{LabelSelector: terraState + "=true"}
Expand All @@ -86,16 +86,17 @@ func cleanupK8sSecrets(t *testing.T) {

delProp := metav1.DeletePropagationBackground
delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
errs := []error{}
var errs []error

for _, secret := range secrets.Items {
key, ok := secret.Labels[terraKey]
labels := secret.GetLabels()
key, ok := labels[terraKey]
if !ok {
continue
}

if key == secretSuffix {
err = sClient.Delete(secret.Name, delOps)
err = sClient.Delete(secret.GetName(), delOps)
if err != nil {
errs = append(errs, err)
}
Expand Down
98 changes: 69 additions & 29 deletions backend/remote-state/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@ import (
"bytes"
"compress/gzip"
"crypto/md5"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/validation"
kubernetes "k8s.io/client-go/kubernetes"
"k8s.io/client-go/dynamic"
)

const (
terraState = "terrastate"
terraKey = "terraKey"
terraWorkspace = "terraWorkspace"
lockInfoKey = "lockInfo"
)

type RemoteClient struct {
k8sClient kubernetes.Interface
k8sClient dynamic.ResourceInterface
namespace string
nameSuffix string
workspace string
Expand All @@ -36,21 +38,25 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
if err != nil {
return nil, err
}
secret, err := c.k8sClient.CoreV1().Secrets(c.namespace).Get(sName, metav1.GetOptions{})
secret, err := c.k8sClient.Get(sName, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
return nil, nil
}
return nil, err
}

stateRaw, ok := secret.Data[terraState]
sData := getSecretData(secret)

stateRaw, ok := sData[terraState]
if !ok {
// The secret exists but there is no state in it
return nil, nil
}

state, err := uncompressState(stateRaw)
stateRawBytes := stateRaw.(string)

state, err := uncompressState([]byte(stateRawBytes))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -88,9 +94,9 @@ func (c *RemoteClient) Put(data []byte) error {
return err
}

secret.Data[terraState] = payload
setTerraState(secret, payload)

secret, err = c.k8sClient.CoreV1().Secrets(c.namespace).Update(secret)
secret, err = c.k8sClient.Update(secret, metav1.UpdateOptions{})
if err != nil {
lockErr := &state.LockError{
Info: lockInfo,
Expand Down Expand Up @@ -134,24 +140,26 @@ func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {

// The secret doesn't exist yet, create it with the lock
sData := make(map[string][]byte)
sData["lockInfo"] = lockInfo
sData[lockInfoKey] = lockInfo

label := map[string]string{
terraState: "true",
terraKey: c.nameSuffix,
terraWorkspace: c.workspace,
}
secret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sName,
Namespace: c.namespace,
Labels: label,
Annotations: map[string]string{"encoding": "gzip"},
secret = &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": metav1.ObjectMeta{
Name: sName,
Namespace: c.namespace,
Labels: label,
Annotations: map[string]string{"encoding": "gzip"},
},
"data": sData,
},
Data: sData,
}

_, err = c.k8sClient.CoreV1().Secrets(c.namespace).Create(secret)
_, err = c.k8sClient.Create(secret, metav1.CreateOptions{})
if err != nil {
if !k8serrors.IsAlreadyExists(err) {
return "", err
Expand Down Expand Up @@ -181,8 +189,9 @@ func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
return "", lockErr
}

secret.Data["lockInfo"] = lockInfo
_, err = c.k8sClient.CoreV1().Secrets(c.namespace).Update(secret)
setLockInfo(secret, lockInfo)

_, err = c.k8sClient.Update(secret, metav1.UpdateOptions{})
if err != nil {
return "", err
}
Expand Down Expand Up @@ -224,9 +233,9 @@ func (c *RemoteClient) Unlock(id string) error {
return lockErr
}

secret.Data["lockInfo"] = []byte{}
setLockInfo(secret, []byte{})

_, err = c.k8sClient.CoreV1().Secrets(c.namespace).Update(secret)
_, err = c.k8sClient.Update(secret, metav1.UpdateOptions{})
if err != nil {
lockErr.Err = err
return lockErr
Expand All @@ -235,30 +244,30 @@ func (c *RemoteClient) Unlock(id string) error {
return nil
}

//getLockInfo takes a secret and attemps to read the lockInfo field.
func (c *RemoteClient) getLockInfo(secret *corev1.Secret) (*state.LockInfo, error) {
lockData, ok := secret.Data["lockInfo"]
//getLockInfo takes a secret and attempts to read the lockInfo field.
func (c *RemoteClient) getLockInfo(secret *unstructured.Unstructured) (*state.LockInfo, error) {
lockData, ok := getLockInfo(secret)
if len(lockData) == 0 || !ok {
return nil, nil
}

lockInfo := &state.LockInfo{}
err := json.Unmarshal([]byte(lockData), lockInfo)
err := json.Unmarshal(lockData, lockInfo)
if err != nil {
return nil, err
}

return lockInfo, nil
}

func (c *RemoteClient) getSecret(name string) (*corev1.Secret, error) {
return c.k8sClient.CoreV1().Secrets(c.namespace).Get(name, metav1.GetOptions{})
func (c *RemoteClient) getSecret(name string) (*unstructured.Unstructured, error) {
return c.k8sClient.Get(name, metav1.GetOptions{})
}

func (c *RemoteClient) deleteSecret(name string) error {
delProp := metav1.DeletePropagationBackground
delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
return c.k8sClient.CoreV1().Secrets(c.namespace).Delete(name, delOps)
return c.k8sClient.Delete(name, delOps)
}

func (c *RemoteClient) createSecretName() (string, error) {
Expand Down Expand Up @@ -289,8 +298,13 @@ func compressState(data []byte) ([]byte, error) {
}

func uncompressState(data []byte) ([]byte, error) {
decode, err := base64.StdEncoding.DecodeString(string(data))
if err != nil {
return nil, err
}

b := new(bytes.Buffer)
gz, err := gzip.NewReader(bytes.NewReader(data))
gz, err := gzip.NewReader(bytes.NewReader(decode))
if err != nil {
return nil, err
}
Expand All @@ -300,3 +314,29 @@ func uncompressState(data []byte) ([]byte, error) {
}
return b.Bytes(), nil
}

func getSecretData(secret *unstructured.Unstructured) map[string]interface{} {
return secret.Object["data"].(map[string]interface{})
}

func getLockInfo(secret *unstructured.Unstructured) ([]byte, bool) {
sData := getSecretData(secret)

info := sData[lockInfoKey].(string)
decode, err := base64.StdEncoding.DecodeString(string(info))
if err != nil {
return nil, false
}

return decode, true
}

func setLockInfo(secret *unstructured.Unstructured, l []byte) {
sData := getSecretData(secret)
sData[lockInfoKey] = l
}

func setTerraState(secret *unstructured.Unstructured, t []byte) {
sData := getSecretData(secret)
sData[terraState] = t
}

0 comments on commit f70d6e1

Please sign in to comment.