github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/remote-state/kubernetes/client.go (about)

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