github.com/crossplane/upjet@v1.3.0/pkg/examples/example.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package examples 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "regexp" 14 "sort" 15 "strings" 16 17 "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 18 xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta" 19 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 20 "github.com/pkg/errors" 21 "sigs.k8s.io/yaml" 22 23 "github.com/crossplane/upjet/pkg/config" 24 "github.com/crossplane/upjet/pkg/registry/reference" 25 "github.com/crossplane/upjet/pkg/resource/json" 26 tjtypes "github.com/crossplane/upjet/pkg/types" 27 "github.com/crossplane/upjet/pkg/types/name" 28 ) 29 30 var ( 31 reFile = regexp.MustCompile(`file\("(.+)"\)`) 32 ) 33 34 const ( 35 labelExampleName = "testing.upbound.io/example-name" 36 annotationExampleGroup = "meta.upbound.io/example-id" 37 defaultExampleName = "example" 38 defaultNamespace = "upbound-system" 39 ) 40 41 // Generator represents a pipeline for generating example manifests. 42 // Generates example manifests for Terraform resources under examples-generated. 43 type Generator struct { 44 reference.Injector 45 rootDir string 46 configResources map[string]*config.Resource 47 resources map[string]*reference.PavedWithManifest 48 } 49 50 // NewGenerator returns a configured Generator 51 func NewGenerator(rootDir, modulePath, shortName string, configResources map[string]*config.Resource) *Generator { 52 return &Generator{ 53 Injector: reference.Injector{ 54 ModulePath: modulePath, 55 ProviderShortName: shortName, 56 }, 57 rootDir: rootDir, 58 configResources: configResources, 59 resources: make(map[string]*reference.PavedWithManifest), 60 } 61 } 62 63 // StoreExamples stores the generated example manifests under examples-generated in 64 // their respective API groups. 65 func (eg *Generator) StoreExamples() error { // nolint:gocyclo 66 for rn, pm := range eg.resources { 67 manifestDir := filepath.Dir(pm.ManifestPath) 68 if err := os.MkdirAll(manifestDir, 0750); err != nil { 69 return errors.Wrapf(err, "cannot mkdir %s", manifestDir) 70 } 71 var buff bytes.Buffer 72 if err := eg.writeManifest(&buff, pm, &reference.ResolutionContext{ 73 WildcardNames: true, 74 Context: eg.resources, 75 }); err != nil { 76 return errors.Wrapf(err, "cannot store example manifest for resource: %s", rn) 77 } 78 if r, ok := eg.configResources[reference.NewRefPartsFromResourceName(rn).Resource]; ok && r.MetaResource != nil { 79 re := r.MetaResource.Examples[0] 80 context, err := reference.PrepareLocalResolutionContext(re, reference.NewRefParts(reference.NewRefPartsFromResourceName(rn).Resource, re.Name).GetResourceName(false)) 81 if err != nil { 82 return errors.Wrapf(err, "cannot prepare local resolution context for resource: %s", rn) 83 } 84 dKeys := make([]string, 0, len(re.Dependencies)) 85 for k := range re.Dependencies { 86 dKeys = append(dKeys, k) 87 } 88 sort.Strings(dKeys) 89 for _, dn := range dKeys { 90 dr, ok := eg.resources[reference.NewRefPartsFromResourceName(dn).GetResourceName(true)] 91 if !ok { 92 continue 93 } 94 var exampleParams map[string]any 95 if err := json.TFParser.Unmarshal([]byte(re.Dependencies[dn]), &exampleParams); err != nil { 96 return errors.Wrapf(err, "cannot unmarshal example manifest for resource: %s", dr.Config.Name) 97 } 98 // e.g. meta.upbound.io/example-id: ec2/v1beta1/instance 99 eGroup := fmt.Sprintf("%s/%s/%s", strings.ToLower(r.ShortGroup), r.Version, strings.ToLower(r.Kind)) 100 pmd := paveCRManifest(exampleParams, dr.Config, 101 reference.NewRefPartsFromResourceName(dn).ExampleName, dr.Group, dr.Version, eGroup) 102 if err := eg.writeManifest(&buff, pmd, context); err != nil { 103 return errors.Wrapf(err, "cannot store example manifest for %s dependency: %s", rn, dn) 104 } 105 } 106 } 107 108 newBuff := bytes.TrimSuffix(buff.Bytes(), []byte("\n---\n\n")) 109 110 // no sensitive info in the example manifest 111 if err := os.WriteFile(pm.ManifestPath, newBuff, 0600); err != nil { 112 return errors.Wrapf(err, "cannot write example manifest file %s for resource %s", pm.ManifestPath, rn) 113 } 114 } 115 return nil 116 } 117 118 func paveCRManifest(exampleParams map[string]any, r *config.Resource, eName, group, version, eGroup string) *reference.PavedWithManifest { 119 delete(exampleParams, "depends_on") 120 delete(exampleParams, "lifecycle") 121 transformFields(r, exampleParams, r.ExternalName.OmittedFields, "") 122 metadata := map[string]any{ 123 "labels": map[string]string{ 124 labelExampleName: eName, 125 }, 126 "annotations": map[string]string{ 127 annotationExampleGroup: eGroup, 128 }, 129 } 130 example := map[string]any{ 131 "apiVersion": fmt.Sprintf("%s/%s", group, version), 132 "kind": r.Kind, 133 "metadata": metadata, 134 "spec": map[string]any{ 135 "forProvider": exampleParams, 136 }, 137 } 138 if len(r.MetaResource.ExternalName) != 0 { 139 metadata["annotations"].(map[string]string)[xpmeta.AnnotationKeyExternalName] = r.MetaResource.ExternalName 140 } 141 return &reference.PavedWithManifest{ 142 Paved: fieldpath.Pave(example), 143 ParamsPrefix: []string{"spec", "forProvider"}, 144 Config: r, 145 Group: group, 146 Version: version, 147 } 148 } 149 150 func dns1123Name(name string) string { 151 return strings.ReplaceAll(strings.ToLower(name), "_", "-") 152 } 153 154 func (eg *Generator) writeManifest(writer io.Writer, pm *reference.PavedWithManifest, resolutionContext *reference.ResolutionContext) error { 155 if err := eg.ResolveReferencesOfPaved(pm, resolutionContext); err != nil { 156 return errors.Wrap(err, "cannot resolve references of resource") 157 } 158 labels, err := pm.Paved.GetValue("metadata.labels") 159 if err != nil { 160 return errors.Wrap(err, `cannot get "metadata.labels" from paved`) 161 } 162 pm.ExampleName = dns1123Name(labels.(map[string]string)[labelExampleName]) 163 if err := pm.Paved.SetValue("metadata.name", pm.ExampleName); err != nil { 164 return errors.Wrapf(err, `cannot set "metadata.name" for resource %q:%s`, pm.Config.Name, pm.ExampleName) 165 } 166 u := pm.Paved.UnstructuredContent() 167 buff, err := yaml.Marshal(u) 168 if err != nil { 169 return errors.Wrap(err, "cannot marshal example resource manifest") 170 } 171 if _, err := writer.Write(buff); err != nil { 172 return errors.Wrap(err, "cannot write resource manifest to the underlying stream") 173 } 174 _, err = writer.Write([]byte("\n---\n\n")) 175 return errors.Wrap(err, "cannot write YAML document separator to the underlying stream") 176 } 177 178 // Generate generates an example manifest for the specified Terraform resource. 179 func (eg *Generator) Generate(group, version string, r *config.Resource) error { 180 rm := eg.configResources[r.Name].MetaResource 181 if rm == nil || len(rm.Examples) == 0 { 182 return nil 183 } 184 groupPrefix := strings.ToLower(strings.Split(group, ".")[0]) 185 // e.g. gvk = ec2/v1beta1/instance 186 gvk := fmt.Sprintf("%s/%s/%s", groupPrefix, version, strings.ToLower(r.Kind)) 187 pm := paveCRManifest(rm.Examples[0].Paved.UnstructuredContent(), r, rm.Examples[0].Name, group, version, gvk) 188 manifestDir := filepath.Join(eg.rootDir, "examples-generated", groupPrefix, r.Version) 189 pm.ManifestPath = filepath.Join(manifestDir, fmt.Sprintf("%s.yaml", strings.ToLower(r.Kind))) 190 eg.resources[fmt.Sprintf("%s.%s", r.Name, reference.Wildcard)] = pm 191 return nil 192 } 193 194 func getHierarchicalName(prefix, name string) string { 195 if prefix == "" { 196 return name 197 } 198 return fmt.Sprintf("%s.%s", prefix, name) 199 } 200 201 func isStatus(r *config.Resource, attr string) bool { 202 s := config.GetSchema(r.TerraformResource, attr) 203 if s == nil { 204 return false 205 } 206 return tjtypes.IsObservation(s) 207 } 208 209 func transformFields(r *config.Resource, params map[string]any, omittedFields []string, namePrefix string) { // nolint:gocyclo 210 for n := range params { 211 hName := getHierarchicalName(namePrefix, n) 212 if isStatus(r, hName) { 213 delete(params, n) 214 continue 215 } 216 for _, hn := range omittedFields { 217 if hn == hName { 218 delete(params, n) 219 break 220 } 221 } 222 } 223 224 for n, v := range params { 225 switch pT := v.(type) { 226 case map[string]any: 227 transformFields(r, pT, omittedFields, getHierarchicalName(namePrefix, n)) 228 229 case []any: 230 for _, e := range pT { 231 eM, ok := e.(map[string]any) 232 if !ok { 233 continue 234 } 235 transformFields(r, eM, omittedFields, getHierarchicalName(namePrefix, n)) 236 } 237 } 238 } 239 240 for n, v := range params { 241 fieldPath := getHierarchicalName(namePrefix, n) 242 sch := config.GetSchema(r.TerraformResource, fieldPath) 243 if sch == nil { 244 continue 245 } 246 // At this point, we confirmed that the field is part of the schema, 247 // so we'll need to perform at least name change on it. 248 delete(params, n) 249 fn := name.NewFromSnake(n) 250 switch { 251 case sch.Sensitive: 252 secretName, secretKey := getSecretRef(v) 253 params[fn.LowerCamelComputed+"SecretRef"] = getRefField(v, map[string]any{ 254 "name": secretName, 255 "namespace": defaultNamespace, 256 "key": secretKey, 257 }) 258 case r.References[fieldPath] != config.Reference{}: 259 switch v.(type) { 260 case []any: 261 l := sch.Type == schema.TypeList || sch.Type == schema.TypeSet 262 ref := name.ReferenceFieldName(fn, l, r.References[fieldPath].RefFieldName) 263 params[ref.LowerCamelComputed] = getNameRefField(v) 264 default: 265 sel := name.SelectorFieldName(fn, r.References[fieldPath].SelectorFieldName) 266 params[sel.LowerCamelComputed] = getSelectorField(v) 267 } 268 default: 269 params[fn.LowerCamelComputed] = v 270 } 271 } 272 } 273 274 func getNameRefField(v any) any { 275 arr := v.([]any) 276 refArr := make([]map[string]any, len(arr)) 277 for i, r := range arr { 278 refArr[i] = map[string]any{ 279 "name": defaultExampleName, 280 } 281 if parts := reference.MatchRefParts(fmt.Sprintf("%v", r)); parts != nil { 282 refArr[i]["name"] = parts.ExampleName 283 } 284 } 285 return refArr 286 } 287 288 func getSelectorField(refVal any) any { 289 ref := map[string]string{ 290 labelExampleName: defaultExampleName, 291 } 292 if parts := reference.MatchRefParts(fmt.Sprintf("%v", refVal)); parts != nil { 293 ref[labelExampleName] = parts.ExampleName 294 } 295 return map[string]any{ 296 "matchLabels": ref, 297 } 298 } 299 300 func getRefField(v any, ref map[string]any) any { 301 switch v.(type) { 302 case []any: 303 return []any{ 304 ref, 305 } 306 307 default: 308 return ref 309 } 310 } 311 312 func getSecretRef(v any) (string, string) { 313 secretName := "example-secret" 314 secretKey := "example-key" 315 s, ok := v.(string) 316 if !ok { 317 return secretName, secretKey 318 } 319 g := reference.ReRef.FindStringSubmatch(s) 320 if len(g) != 2 { 321 return secretName, secretKey 322 } 323 f := reFile.FindStringSubmatch(g[1]) 324 switch { 325 case len(f) == 2: // then a file reference 326 _, file := filepath.Split(f[1]) 327 secretKey = fmt.Sprintf("attribute.%s", file) 328 default: 329 parts := strings.Split(g[1], ".") 330 if len(parts) < 3 { 331 return secretName, secretKey 332 } 333 secretName = fmt.Sprintf("example-%s", strings.Join(strings.Split(parts[0], "_")[1:], "-")) 334 secretKey = fmt.Sprintf("attribute.%s", strings.Join(parts[2:], ".")) 335 } 336 return secretName, secretKey 337 }