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 }