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 }