github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/kubernetes/client.go (about)

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