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