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 }