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