kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/remote-state/kubernetes/client.go (about)

     1  package kubernetes
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"crypto/md5"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"strings"
    12  
    13  	"kubeform.dev/terraform-backend-sdk/states/remote"
    14  	"kubeform.dev/terraform-backend-sdk/states/statemgr"
    15  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    18  	"k8s.io/apimachinery/pkg/util/validation"
    19  	"k8s.io/client-go/dynamic"
    20  	_ "k8s.io/client-go/plugin/pkg/client/auth" // Import to initialize client auth plugins.
    21  	"k8s.io/utils/pointer"
    22  
    23  	coordinationv1 "k8s.io/api/coordination/v1"
    24  	coordinationclientv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
    25  )
    26  
    27  const (
    28  	tfstateKey                = "tfstate"
    29  	tfstateSecretSuffixKey    = "tfstateSecretSuffix"
    30  	tfstateWorkspaceKey       = "tfstateWorkspace"
    31  	tfstateLockInfoAnnotation = "app.terraform.io/lock-info"
    32  	managedByKey              = "app.kubernetes.io/managed-by"
    33  )
    34  
    35  type RemoteClient struct {
    36  	kubernetesSecretClient dynamic.ResourceInterface
    37  	kubernetesLeaseClient  coordinationclientv1.LeaseInterface
    38  	namespace              string
    39  	labels                 map[string]string
    40  	nameSuffix             string
    41  	workspace              string
    42  }
    43  
    44  func (c *RemoteClient) Get() (payload *remote.Payload, err error) {
    45  	secretName, err := c.createSecretName()
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	secret, err := c.kubernetesSecretClient.Get(secretName, metav1.GetOptions{})
    50  	if err != nil {
    51  		if k8serrors.IsNotFound(err) {
    52  			return nil, nil
    53  		}
    54  		return nil, err
    55  	}
    56  
    57  	secretData := getSecretData(secret)
    58  	stateRaw, ok := secretData[tfstateKey]
    59  	if !ok {
    60  		// The secret exists but there is no state in it
    61  		return nil, nil
    62  	}
    63  
    64  	stateRawString := stateRaw.(string)
    65  
    66  	state, err := uncompressState(stateRawString)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	md5 := md5.Sum(state)
    72  
    73  	p := &remote.Payload{
    74  		Data: state,
    75  		MD5:  md5[:],
    76  	}
    77  	return p, nil
    78  }
    79  
    80  func (c *RemoteClient) Put(data []byte) error {
    81  	secretName, err := c.createSecretName()
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	payload, err := compressState(data)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	secret, err := c.getSecret(secretName)
    92  	if err != nil {
    93  		if !k8serrors.IsNotFound(err) {
    94  			return err
    95  		}
    96  
    97  		secret = &unstructured.Unstructured{
    98  			Object: map[string]interface{}{
    99  				"metadata": metav1.ObjectMeta{
   100  					Name:        secretName,
   101  					Namespace:   c.namespace,
   102  					Labels:      c.getLabels(),
   103  					Annotations: map[string]string{"encoding": "gzip"},
   104  				},
   105  			},
   106  		}
   107  
   108  		secret, err = c.kubernetesSecretClient.Create(secret, metav1.CreateOptions{})
   109  		if err != nil {
   110  			return err
   111  		}
   112  	}
   113  
   114  	setState(secret, payload)
   115  	_, err = c.kubernetesSecretClient.Update(secret, metav1.UpdateOptions{})
   116  	return err
   117  }
   118  
   119  // Delete the state secret
   120  func (c *RemoteClient) Delete() error {
   121  	secretName, err := c.createSecretName()
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	err = c.deleteSecret(secretName)
   127  	if err != nil {
   128  		if !k8serrors.IsNotFound(err) {
   129  			return err
   130  		}
   131  	}
   132  
   133  	leaseName, err := c.createLeaseName()
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	err = c.deleteLease(leaseName)
   139  	if err != nil {
   140  		if !k8serrors.IsNotFound(err) {
   141  			return err
   142  		}
   143  	}
   144  	return nil
   145  }
   146  
   147  func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
   148  	leaseName, err := c.createLeaseName()
   149  	if err != nil {
   150  		return "", err
   151  	}
   152  
   153  	lease, err := c.getLease(leaseName)
   154  	if err != nil {
   155  		if !k8serrors.IsNotFound(err) {
   156  			return "", err
   157  		}
   158  
   159  		labels := c.getLabels()
   160  		lease = &coordinationv1.Lease{
   161  			ObjectMeta: metav1.ObjectMeta{
   162  				Name:   leaseName,
   163  				Labels: labels,
   164  				Annotations: map[string]string{
   165  					tfstateLockInfoAnnotation: string(info.Marshal()),
   166  				},
   167  			},
   168  			Spec: coordinationv1.LeaseSpec{
   169  				HolderIdentity: pointer.StringPtr(info.ID),
   170  			},
   171  		}
   172  
   173  		_, err = c.kubernetesLeaseClient.Create(lease)
   174  		if err != nil {
   175  			return "", err
   176  		} else {
   177  			return info.ID, nil
   178  		}
   179  	}
   180  
   181  	if lease.Spec.HolderIdentity != nil {
   182  		if *lease.Spec.HolderIdentity == info.ID {
   183  			return info.ID, nil
   184  		}
   185  
   186  		currentLockInfo, err := c.getLockInfo(lease)
   187  		if err != nil {
   188  			return "", err
   189  		}
   190  
   191  		lockErr := &statemgr.LockError{
   192  			Info: currentLockInfo,
   193  			Err:  errors.New("the state is already locked by another terraform client"),
   194  		}
   195  		return "", lockErr
   196  	}
   197  
   198  	lease.Spec.HolderIdentity = pointer.StringPtr(info.ID)
   199  	setLockInfo(lease, info.Marshal())
   200  	_, err = c.kubernetesLeaseClient.Update(lease)
   201  	if err != nil {
   202  		return "", err
   203  	}
   204  
   205  	return info.ID, err
   206  }
   207  
   208  func (c *RemoteClient) Unlock(id string) error {
   209  	leaseName, err := c.createLeaseName()
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	lease, err := c.getLease(leaseName)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	if lease.Spec.HolderIdentity == nil {
   220  		return fmt.Errorf("state is already unlocked")
   221  	}
   222  
   223  	lockInfo, err := c.getLockInfo(lease)
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	lockErr := &statemgr.LockError{Info: lockInfo}
   229  	if *lease.Spec.HolderIdentity != id {
   230  		lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
   231  		return lockErr
   232  	}
   233  
   234  	lease.Spec.HolderIdentity = nil
   235  	removeLockInfo(lease)
   236  
   237  	_, err = c.kubernetesLeaseClient.Update(lease)
   238  	if err != nil {
   239  		lockErr.Err = err
   240  		return lockErr
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  func (c *RemoteClient) getLockInfo(lease *coordinationv1.Lease) (*statemgr.LockInfo, error) {
   247  	lockData, ok := getLockInfo(lease)
   248  	if len(lockData) == 0 || !ok {
   249  		return nil, nil
   250  	}
   251  
   252  	lockInfo := &statemgr.LockInfo{}
   253  	err := json.Unmarshal(lockData, lockInfo)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	return lockInfo, nil
   259  }
   260  
   261  func (c *RemoteClient) getLabels() map[string]string {
   262  	l := map[string]string{
   263  		tfstateKey:             "true",
   264  		tfstateSecretSuffixKey: c.nameSuffix,
   265  		tfstateWorkspaceKey:    c.workspace,
   266  		managedByKey:           "terraform",
   267  	}
   268  
   269  	if len(c.labels) != 0 {
   270  		for k, v := range c.labels {
   271  			l[k] = v
   272  		}
   273  	}
   274  
   275  	return l
   276  }
   277  
   278  func (c *RemoteClient) getSecret(name string) (*unstructured.Unstructured, error) {
   279  	return c.kubernetesSecretClient.Get(name, metav1.GetOptions{})
   280  }
   281  
   282  func (c *RemoteClient) getLease(name string) (*coordinationv1.Lease, error) {
   283  	return c.kubernetesLeaseClient.Get(name, metav1.GetOptions{})
   284  }
   285  
   286  func (c *RemoteClient) deleteSecret(name string) error {
   287  	secret, err := c.getSecret(name)
   288  	if err != nil {
   289  		return err
   290  	}
   291  
   292  	labels := secret.GetLabels()
   293  	v, ok := labels[tfstateKey]
   294  	if !ok || v != "true" {
   295  		return fmt.Errorf("Secret does does not have %q label", tfstateKey)
   296  	}
   297  
   298  	delProp := metav1.DeletePropagationBackground
   299  	delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
   300  	return c.kubernetesSecretClient.Delete(name, delOps)
   301  }
   302  
   303  func (c *RemoteClient) deleteLease(name string) error {
   304  	secret, err := c.getLease(name)
   305  	if err != nil {
   306  		return err
   307  	}
   308  
   309  	labels := secret.GetLabels()
   310  	v, ok := labels[tfstateKey]
   311  	if !ok || v != "true" {
   312  		return fmt.Errorf("Lease does does not have %q label", tfstateKey)
   313  	}
   314  
   315  	delProp := metav1.DeletePropagationBackground
   316  	delOps := &metav1.DeleteOptions{PropagationPolicy: &delProp}
   317  	return c.kubernetesLeaseClient.Delete(name, delOps)
   318  }
   319  
   320  func (c *RemoteClient) createSecretName() (string, error) {
   321  	secretName := strings.Join([]string{tfstateKey, c.workspace, c.nameSuffix}, "-")
   322  
   323  	errs := validation.IsDNS1123Subdomain(secretName)
   324  	if len(errs) > 0 {
   325  		k8sInfo := `
   326  This is a requirement for Kubernetes secret names. 
   327  The workspace name and key must adhere to Kubernetes naming conventions.`
   328  		msg := fmt.Sprintf("the secret name %v is invalid, ", secretName)
   329  		return "", errors.New(msg + strings.Join(errs, ",") + k8sInfo)
   330  	}
   331  
   332  	return secretName, nil
   333  }
   334  
   335  func (c *RemoteClient) createLeaseName() (string, error) {
   336  	n, err := c.createSecretName()
   337  	if err != nil {
   338  		return "", err
   339  	}
   340  	return "lock-" + n, nil
   341  }
   342  
   343  func compressState(data []byte) ([]byte, error) {
   344  	b := new(bytes.Buffer)
   345  	gz := gzip.NewWriter(b)
   346  	if _, err := gz.Write(data); err != nil {
   347  		return nil, err
   348  	}
   349  	if err := gz.Close(); err != nil {
   350  		return nil, err
   351  	}
   352  	return b.Bytes(), nil
   353  }
   354  
   355  func uncompressState(data string) ([]byte, error) {
   356  	decode, err := base64.StdEncoding.DecodeString(data)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	b := new(bytes.Buffer)
   362  	gz, err := gzip.NewReader(bytes.NewReader(decode))
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	b.ReadFrom(gz)
   367  	if err := gz.Close(); err != nil {
   368  		return nil, err
   369  	}
   370  	return b.Bytes(), nil
   371  }
   372  
   373  func getSecretData(secret *unstructured.Unstructured) map[string]interface{} {
   374  	if m, ok := secret.Object["data"].(map[string]interface{}); ok {
   375  		return m
   376  	}
   377  	return map[string]interface{}{}
   378  }
   379  
   380  func getLockInfo(lease *coordinationv1.Lease) ([]byte, bool) {
   381  	info, ok := lease.ObjectMeta.GetAnnotations()[tfstateLockInfoAnnotation]
   382  	if !ok {
   383  		return nil, false
   384  	}
   385  	return []byte(info), true
   386  }
   387  
   388  func setLockInfo(lease *coordinationv1.Lease, l []byte) {
   389  	annotations := lease.ObjectMeta.GetAnnotations()
   390  	if annotations != nil {
   391  		annotations[tfstateLockInfoAnnotation] = string(l)
   392  	} else {
   393  		annotations = map[string]string{
   394  			tfstateLockInfoAnnotation: string(l),
   395  		}
   396  	}
   397  	lease.ObjectMeta.SetAnnotations(annotations)
   398  }
   399  
   400  func removeLockInfo(lease *coordinationv1.Lease) {
   401  	annotations := lease.ObjectMeta.GetAnnotations()
   402  	delete(annotations, tfstateLockInfoAnnotation)
   403  	lease.ObjectMeta.SetAnnotations(annotations)
   404  }
   405  
   406  func setState(secret *unstructured.Unstructured, t []byte) {
   407  	secretData := getSecretData(secret)
   408  	secretData[tfstateKey] = t
   409  	secret.Object["data"] = secretData
   410  }