github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/kubernetes/client.go (about)

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