github.com/crossplane/upjet@v1.3.0/pkg/resource/sensitive.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package resource 6 7 import ( 8 "context" 9 "fmt" 10 "regexp" 11 "strings" 12 13 v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 14 "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 15 "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 16 "github.com/crossplane/crossplane-runtime/pkg/resource" 17 "github.com/pkg/errors" 18 kerrors "k8s.io/apimachinery/pkg/api/errors" 19 "k8s.io/apimachinery/pkg/runtime" 20 21 "github.com/crossplane/upjet/pkg/config" 22 ) 23 24 const ( 25 errCannotExpandWildcards = "cannot expand wildcards" 26 errFmtCannotGetValueForFieldPath = "cannot not get a value for fieldpath %q" 27 errFmtCannotGetStringForFieldPath = "cannot not get a string for fieldpath %q" 28 errFmtCannotGetSecretKeySelector = "cannot get SecretKeySelector from xp resource for fieldpath %q" 29 errFmtCannotGetSecretKeySelectorAsList = "cannot get SecretKeySelector list from xp resource for fieldpath %q" 30 errFmtCannotGetSecretKeySelectorAsMap = "cannot get SecretKeySelector map from xp resource for fieldpath %q" 31 errFmtCannotGetSecretValue = "cannot get secret value for %v" 32 ) 33 34 const ( 35 // prefixAttribute used to prefix connection detail keys for sensitive 36 // Terraform attributes. We need this prefix to ensure that they are not 37 // overridden by any custom connection key configured which would break 38 // our ability to build tfstate back. 39 prefixAttribute = "attribute." 40 41 pluralSuffix = "s" 42 43 errGetAdditionalConnectionDetails = "cannot get additional connection details" 44 errFmtCannotOverrideExistingKey = "overriding a reserved connection key (%q) is not allowed" 45 ) 46 47 var reEndsWithIndex *regexp.Regexp 48 var reMiddleIndex *regexp.Regexp 49 var reInsideThreeDotsBlock *regexp.Regexp 50 51 func init() { 52 reEndsWithIndex = regexp.MustCompile(`\.(\d+?)$`) 53 reMiddleIndex = regexp.MustCompile(`\.(\d+?)\.`) 54 reInsideThreeDotsBlock = regexp.MustCompile(`\.\.\.(.*?)\.\.\.`) 55 } 56 57 // SecretClient is the client to get sensitive data from kubernetes secrets 58 // 59 //go:generate go run github.com/golang/mock/mockgen -copyright_file ../../hack/boilerplate.txt -destination ./fake/mocks/mock.go -package mocks github.com/crossplane/upjet/pkg/resource SecretClient 60 type SecretClient interface { 61 GetSecretData(ctx context.Context, ref *v1.SecretReference) (map[string][]byte, error) 62 GetSecretValue(ctx context.Context, sel v1.SecretKeySelector) ([]byte, error) 63 } 64 65 // GetConnectionDetails returns connection details including the sensitive 66 // Terraform attributes and additions connection details configured. 67 func GetConnectionDetails(attr map[string]any, tr Terraformed, cfg *config.Resource) (managed.ConnectionDetails, error) { 68 conn, err := GetSensitiveAttributes(attr, tr.GetConnectionDetailsMapping()) 69 if err != nil { 70 return nil, errors.Wrap(err, "cannot get connection details") 71 } 72 73 add, err := cfg.Sensitive.AdditionalConnectionDetailsFn(attr) 74 if err != nil { 75 return nil, errors.Wrap(err, errGetAdditionalConnectionDetails) 76 } 77 for k, v := range add { 78 if _, ok := conn[k]; ok { 79 // We return error if a custom key tries to override an existing 80 // connection key. This is because we use connection keys to rebuild 81 // the tfstate, i.e. otherwise we would lose the original value in 82 // tfstate. 83 // Indeed, we are prepending "attribute_" to the Terraform 84 // state sensitive keys and connection keys starting with this 85 // prefix are reserved and should not be used as a custom connection 86 // key. 87 return nil, errors.Errorf(errFmtCannotOverrideExistingKey, k) 88 } 89 if conn == nil { 90 conn = map[string][]byte{} 91 } 92 conn[k] = v 93 } 94 95 return conn, nil 96 } 97 98 // GetSensitiveAttributes returns strings matching provided field paths in the 99 // input data. 100 // See the unit tests for examples. 101 func GetSensitiveAttributes(from map[string]any, mapping map[string]string) (map[string][]byte, error) { //nolint: gocyclo 102 if len(mapping) == 0 { 103 return nil, nil 104 } 105 paved := fieldpath.Pave(from) 106 var vals map[string][]byte 107 for tf := range mapping { 108 fieldPaths, err := paved.ExpandWildcards(tf) 109 if err != nil { 110 if fieldpath.IsNotFound(err) { 111 continue 112 } 113 return nil, errors.Wrap(err, errCannotExpandWildcards) 114 } 115 116 for _, fp := range fieldPaths { 117 v, err := paved.GetValue(fp) 118 if err != nil { 119 return nil, errors.Wrapf(err, errFmtCannotGetValueForFieldPath, fp) 120 } 121 // Gracefully skip if v is nil which implies that this field is 122 // optional and not provided. 123 if v == nil { 124 continue 125 } 126 127 // Note(turkenh): k8s secrets uses a strict regex to validate secret 128 // keys which does not allow having brackets inside. So, we need to 129 // do a conversion to be able to store as connection secret keys. 130 // See https://github.com/crossplane/upjet/pull/94 for 131 // more details. 132 k, err := fieldPathToSecretKey(fp) 133 if err != nil { 134 return nil, errors.Wrapf(err, "cannot convert fieldpath %q to secret key", fp) 135 } 136 if vals == nil { 137 vals = map[string][]byte{} 138 } 139 switch s := v.(type) { 140 case map[string]any: 141 for i, e := range s { 142 if err := setSensitiveAttributesToValuesMap(e, i, k, fp, vals); err != nil { 143 return nil, err 144 } 145 } 146 case []any: 147 for i, e := range s { 148 if err := setSensitiveAttributesToValuesMap(e, i, k, fp, vals); err != nil { 149 return nil, err 150 } 151 } 152 case string: 153 vals[fmt.Sprintf("%s%s", prefixAttribute, k)] = []byte(s) 154 default: 155 return nil, errors.Errorf(errFmtCannotGetStringForFieldPath, fp) 156 } 157 } 158 } 159 return vals, nil 160 } 161 162 // GetSensitiveParameters will collect sensitive information as terraform state 163 // attributes by following secret references in the spec. 164 func GetSensitiveParameters(ctx context.Context, client SecretClient, from runtime.Object, into map[string]any, mapping map[string]string) error { //nolint: gocyclo 165 // Note(turkenh): Cyclomatic complexity of this function is slightly higher 166 // than the threshold but preferred to use nolint directive for better 167 // readability and not to split the logic. 168 169 if len(mapping) == 0 { 170 return nil 171 } 172 173 pavedJSON, err := fieldpath.PaveObject(from) 174 if err != nil { 175 return err 176 } 177 pavedTF := fieldpath.Pave(into) 178 179 var sensitive []byte 180 for tfPath, jsonPath := range mapping { 181 jsonPathSet, err := pavedJSON.ExpandWildcards(jsonPath) 182 if err != nil { 183 return errors.Wrapf(err, "cannot expand wildcard for xp resource") 184 } 185 for _, expandedJSONPath := range jsonPathSet { 186 v, err := pavedJSON.GetValue(expandedJSONPath) 187 if err != nil { 188 return errors.Wrapf(err, errFmtCannotGetValueForFieldPath, expandedJSONPath) 189 } 190 // ExpandWildcards call above already skips "nested" optional fields 191 // as they won't be available in the data but added this as an 192 // additional check here. Please note, here all path starts with 193 // spec.forProvider., so, all is "nested" different from GetAttributes 194 if v == nil { 195 continue 196 } 197 198 switch k := v.(type) { 199 case map[string]any: 200 _, ok := k["key"] 201 if !ok { 202 // This is a special case where we have a "SecretReference" without a selected "key". This happens 203 // when there is an input field of type map[string]string (or map[string]*string). 204 // In this case, we need to get the entire secret data and fill it in the terraform state as a map. 205 // This is the only case where we have one-to-many mapping between json and tf paths. 206 ref := &v1.SecretReference{} 207 if err = pavedJSON.GetValueInto(expandedJSONPath, ref); err != nil { 208 return errors.Wrapf(err, errFmtCannotGetSecretKeySelectorAsMap, expandedJSONPath) 209 } 210 data, err := client.GetSecretData(ctx, ref) 211 // We don't want to fail if the secret is not found. Otherwise, we won't be able to delete the 212 // resource if secret is deleted before. This is quite expected when both secret and resource 213 // got deleted in parallel. 214 if resource.IgnoreNotFound(err) != nil { 215 return errors.Wrapf(err, errFmtCannotGetSecretValue, ref) 216 } 217 for key, value := range data { 218 if err = pavedTF.SetValue(fmt.Sprintf("%s.%s", tfPath, key), string(value)); err != nil { 219 return errors.Wrapf(err, "cannot set string as terraform attribute for fieldpath %q", fmt.Sprintf("%s.%s", tfPath, key)) 220 } 221 } 222 continue 223 } 224 225 sel := &v1.SecretKeySelector{} 226 if err = pavedJSON.GetValueInto(expandedJSONPath, sel); err != nil { 227 return errors.Wrapf(err, errFmtCannotGetSecretKeySelector, expandedJSONPath) 228 } 229 sensitive, err = client.GetSecretValue(ctx, *sel) 230 if resource.IgnoreNotFound(err) != nil { 231 return errors.Wrapf(err, errFmtCannotGetSecretValue, sel) 232 } 233 if err := setSensitiveParametersWithPaved(pavedTF, expandedJSONPath, tfPath, mapping, string(sensitive)); err != nil { 234 return err 235 } 236 case []any: 237 sel := &[]v1.SecretKeySelector{} 238 if err = pavedJSON.GetValueInto(expandedJSONPath, sel); err != nil { 239 return errors.Wrapf(err, errFmtCannotGetSecretKeySelectorAsList, expandedJSONPath) 240 } 241 var sensitives []any 242 for _, s := range *sel { 243 sensitive, err = client.GetSecretValue(ctx, s) 244 if resource.IgnoreNotFound(err) != nil { 245 return errors.Wrapf(err, errFmtCannotGetSecretValue, sel) 246 } 247 248 // If referenced k8s secret is deleted before the MR, we pass empty string for the sensitive 249 // field to be able to destroy the resource. 250 if kerrors.IsNotFound(err) { 251 sensitive = []byte("") 252 } 253 sensitives = append(sensitives, string(sensitive)) 254 } 255 if err := setSensitiveParametersWithPaved(pavedTF, expandedJSONPath, tfPath, mapping, sensitives); err != nil { 256 return err 257 } 258 default: 259 return errors.Wrapf(err, errFmtCannotGetSecretKeySelector, expandedJSONPath) 260 } 261 } 262 } 263 264 return nil 265 } 266 267 // GetSensitiveObservation will return sensitive information as terraform state 268 // attributes by reading them from connection details. 269 func GetSensitiveObservation(ctx context.Context, client SecretClient, from *v1.SecretReference, into map[string]any) error { 270 if from == nil { 271 // No secret reference set 272 return nil 273 } 274 conn, err := client.GetSecretData(ctx, from) 275 if kerrors.IsNotFound(err) { 276 // Secret not available/created yet 277 return nil 278 } 279 if err != nil { 280 return errors.Wrapf(err, "cannot get connection secret") 281 } 282 283 paveTF := fieldpath.Pave(into) 284 for k, v := range conn { 285 if !strings.HasPrefix(k, prefixAttribute) { 286 // this is not an attribute key (e.g. custom key), we don't put it 287 // into tfstate attributes. 288 continue 289 } 290 fp, err := secretKeyToFieldPath(strings.TrimPrefix(k, prefixAttribute)) 291 if err != nil { 292 return errors.Wrapf(err, "cannot convert secret key %q to fieldpath", k) 293 } 294 if err = paveTF.SetString(fp, string(v)); err != nil { 295 return errors.Wrapf(err, "cannot set sensitive string in tf attributes for fieldpath %q", fp) 296 } 297 } 298 return nil 299 } 300 301 func expandedTFPath(expandedXP string, mapping map[string]string) (string, error) { 302 sExp, err := fieldpath.Parse(normalizeJSONPath(expandedXP)) 303 if err != nil { 304 return "", err 305 } 306 tfWildcard := "" 307 for tf, xp := range mapping { 308 sxp, err := fieldpath.Parse(normalizeJSONPath(xp)) 309 if err != nil { 310 return "", err 311 } 312 if expandedFor(sExp, sxp) { 313 tfWildcard = tf 314 break 315 } 316 } 317 if tfWildcard == "" { 318 return "", errors.Errorf("cannot find corresponding fieldpath mapping for %q", expandedXP) 319 } 320 sTF, err := fieldpath.Parse(tfWildcard) 321 if err != nil { 322 return "", err 323 } 324 for i, s := range sTF { 325 if s.Field == "*" { 326 sTF[i] = sExp[i] 327 } 328 } 329 330 return sTF.String(), nil 331 } 332 333 func expandedFor(expanded fieldpath.Segments, withWildcard fieldpath.Segments) bool { 334 if len(withWildcard) != len(expanded) { 335 return false 336 } 337 for i, w := range withWildcard { 338 exp := expanded[i] 339 if w.Field == "*" { 340 continue 341 } 342 if w.Type != exp.Type { 343 return false 344 } 345 if w.Field != exp.Field { 346 return false 347 } 348 if w.Index != exp.Index { 349 return false 350 } 351 } 352 return true 353 } 354 355 func normalizeJSONPath(s string) string { 356 return strings.TrimPrefix(strings.TrimPrefix(s, "spec.forProvider."), "status.atProvider.") 357 } 358 359 func secretKeyToFieldPath(s string) (string, error) { 360 s1 := reInsideThreeDotsBlock.ReplaceAllString(s, "[$1]") 361 s2 := reEndsWithIndex.ReplaceAllString(s1, "[$1]") 362 s3 := reMiddleIndex.ReplaceAllString(s2, "[$1].") 363 seg, err := fieldpath.Parse(s3) 364 if err != nil { 365 return "", errors.Wrapf(err, "cannot parse secret key %q as fieldpath", s3) 366 } 367 return seg.String(), nil 368 } 369 370 func fieldPathToSecretKey(s string) (string, error) { 371 sg, err := fieldpath.Parse(s) 372 if err != nil { 373 return "", errors.Wrapf(err, "cannot parse %q as fieldpath", s) 374 } 375 376 var b strings.Builder 377 for _, s := range sg { 378 switch s.Type { 379 case fieldpath.SegmentField: 380 if strings.ContainsRune(s.Field, '.') { 381 b.WriteString(fmt.Sprintf("...%s...", s.Field)) 382 continue 383 } 384 b.WriteString(fmt.Sprintf(".%s", s.Field)) 385 case fieldpath.SegmentIndex: 386 b.WriteString(fmt.Sprintf(".%d", s.Index)) 387 } 388 } 389 390 return strings.TrimPrefix(b.String(), "."), nil 391 } 392 393 func setSensitiveParametersWithPaved(pavedTF *fieldpath.Paved, expandedJSONPath, tfPath string, mapping map[string]string, sensitives any) error { 394 expTF, err := expandedTFPath(expandedJSONPath, mapping) 395 if err != nil { 396 return err 397 } 398 if err = pavedTF.SetValue(expTF, sensitives); err != nil { 399 return errors.Wrapf(err, "cannot set string as terraform attribute for fieldpath %q", tfPath) 400 } 401 return nil 402 } 403 404 func setSensitiveAttributesToValuesMap(e, i any, k, fp string, vals map[string][]byte) error { 405 k = strings.TrimSuffix(k, pluralSuffix) 406 value, ok := e.(string) 407 if !ok { 408 return errors.Errorf(errFmtCannotGetStringForFieldPath, fp) 409 } 410 vals[fmt.Sprintf("%s%s.%v", prefixAttribute, k, i)] = []byte(value) 411 return nil 412 }