v2/tools/generator/internal/codegen/embeddedresources/remover.go (281 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package embeddedresources
import (
"fmt"
"github.com/go-logr/logr"
"github.com/rotisserie/eris"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/config"
)
type resourceRemovalVisitorContext struct {
resource astmodel.InternalTypeName
name astmodel.InternalTypeName
depth int
modifiedDefinitions astmodel.TypeDefinitionSet
}
func (e resourceRemovalVisitorContext) WithMoreDepth() resourceRemovalVisitorContext {
e.depth += 1
// Note that e.modifiedDefinitions is a pointer and so is shared between all instances
// in order to allow tracking what definitions have been modified.
return e
}
func (e resourceRemovalVisitorContext) WithName(name astmodel.InternalTypeName) resourceRemovalVisitorContext {
e.name = name
return e
}
// EmbeddedResourceRemover uses the "x-ms-azure-resource" extension to detect resources that are embedded
// inside other resources.
// There are two different kinds of embeddings:
// 1. Peer resource embedding: This embedding allows users to create the peer resource inline rather than
// use an ARM ID reference. An example of this embedding can be seen at
// https://github.com/Azure/azure-rest-api-specs/blob/main/specification/network/resource-manager/Microsoft.Network/stable/2020-11-01/publicIpAddress.json#L453
// where PublicIPAddress has a natGateway property which refers to "./natGateway.json#/definitions/NatGateway".
// This results in the ability to define a NatGateway inline while creating a PublicIPAddress. This is possibly
// useful in ARM templates but for our purposes would be better served with just an ID string representing the
// NatGateway.
// 2. A subresource embedding. For the same reasons above, embedded sub-resources don't make sense in Kubernetes.
// In the case of embedded sub-resources, the ideal shape would be a complete removal of the reference. We forbid
// parent resources directly referencing child resources as it complicates the Watches scenario for each resource
// reconciler. It's also not a common pattern in Kubernetes - usually you can identify children for a
// given parent via a label. An example of this type of embedding is
// https://github.com/Azure/azure-rest-api-specs/blob/main/specification/network/resource-manager/Microsoft.Network/stable/2020-11-01/routeTable.json#L668
// The Routes property is of type RouteTableRoutes which is a child resource of RouteTable.
type EmbeddedResourceRemover struct {
definitions astmodel.TypeDefinitionSet
resourceToSubresourceMap map[resourceKey]astmodel.TypeNameSet
typeSuffix string
typeFlag astmodel.TypeFlag
// resourcesWhichOwnSubresourceLifecycle is a collection of resources which own 1 or more subresource lifecycles.
// Examples of this include VirtualNetwork (owns Subnet) and RouteTable (owns Route). Ownership in this case means that
// the property subnets/routes on the parent will delete subresources if that collection doesn't include all existing
// subnets/routes.
// This is a map of resource name to details about the owned resource
resourcesWhichOwnSubresourceLifecycle map[astmodel.InternalTypeName][]misbehavingResourceDetails
// resourcesEmbeddedInParent is a collection of subresources whose IsResource() is conditional based on the context it's used in.
// In some places, it is a resource and should be pruned. In other places, it must not be pruned. This usually boils down
// to pseudo-resources in networking that while they look like a resource can only be created as properties on another
// resource and so must NOT be pruned from that context as otherwise they cannot be created anywhere.
// This map is from subresource type name to resource type name.
resourcesEmbeddedInParent map[astmodel.InternalTypeName]astmodel.InternalTypeName
renames map[astmodel.InternalTypeName]embeddedResourceTypeName // A set of all the type renames made, indexed by the new name
}
// MakeEmbeddedResourceRemover creates an EmbeddedResourceRemover for the specified astmodel.TypeDefinitionSet collection.
func MakeEmbeddedResourceRemover(configuration *config.Configuration, definitions astmodel.TypeDefinitionSet) (EmbeddedResourceRemover, error) {
resourceToSubresourceMap := findResourceSubResources(definitions)
resourcesEmbeddedInParent, err := findResourcesEmbeddedInParent(configuration, definitions)
if err != nil {
return EmbeddedResourceRemover{}, eris.Wrap(err, "couldn't find all resources embedded in parent")
}
misbehavingResources, err := findMisbehavingResources(configuration, definitions)
if err != nil {
return EmbeddedResourceRemover{}, eris.Wrap(err, "couldn't find all misbehaving embedded resources")
}
remover := EmbeddedResourceRemover{
definitions: definitions,
resourcesWhichOwnSubresourceLifecycle: misbehavingResources,
resourcesEmbeddedInParent: resourcesEmbeddedInParent,
resourceToSubresourceMap: resourceToSubresourceMap,
typeSuffix: "SubResourceEmbedded",
typeFlag: astmodel.TypeFlag("embeddedSubResource"),
renames: make(map[astmodel.InternalTypeName]embeddedResourceTypeName),
}
return remover, nil
}
// RemoveEmbeddedResources removes any embedded resources according to the
func (e EmbeddedResourceRemover) RemoveEmbeddedResources(
log logr.Logger,
) (astmodel.TypeDefinitionSet, error) {
result := make(astmodel.TypeDefinitionSet, len(e.definitions))
originalNames := make(map[astmodel.InternalTypeName]embeddedResourceTypeName, len(e.definitions)/2)
visitor := e.makeEmbeddedResourceRemovalTypeVisitor()
for _, def := range e.definitions.AllResources() {
typeWalker := e.newResourceRemovalTypeWalker(visitor, def)
updatedTypes, err := typeWalker.Walk(def)
if err != nil {
return nil, err
}
for _, newDef := range updatedTypes {
err := result.AddAllowDuplicates(newDef)
if err != nil {
return nil, err
}
}
// Aggregate all renames
for nw, og := range e.renames {
originalNames[nw] = og
}
}
result, err := simplifyTypeNames(result, e.typeFlag, originalNames, log)
if err != nil {
return nil, err
}
return RemoveEmptyObjects(result, log)
}
func (e EmbeddedResourceRemover) makeEmbeddedResourceRemovalTypeVisitor() astmodel.TypeVisitor[resourceRemovalVisitorContext] {
visitor := astmodel.TypeVisitorBuilder[resourceRemovalVisitorContext]{
VisitObjectType: func(
this *astmodel.TypeVisitor[resourceRemovalVisitorContext],
it *astmodel.ObjectType,
ctx resourceRemovalVisitorContext,
) (astmodel.Type, error) {
parent := e.resourcesEmbeddedInParent[ctx.name]
isResourceEmbeddedInParent := astmodel.TypeEquals(parent, ctx.resource)
if isResourceEmbeddedInParent {
// Remove the ID field if there is one, and it's not a status type.
// The expectation is that these resources must be created through
// their parent, so you won't ever use the ID field
if !ctx.name.IsStatus() {
it = it.WithoutSpecificProperties("Id")
}
return astmodel.OrderedIdentityVisitOfObjectType(this, it, ctx)
}
// If this resource has any properties that are flagged as misbehaving embedded resources, we have to skip
// pruning that property
if detailsCollection, ok := e.resourcesWhichOwnSubresourceLifecycle[ctx.resource]; ok {
for _, details := range detailsCollection {
if details.propertyType == ctx.name {
return astmodel.OrderedIdentityVisitOfObjectType(this, it, ctx)
}
}
}
if ctx.depth <= 1 || !it.IsResource() {
// Avoid removing top level Spec
return astmodel.OrderedIdentityVisitOfObjectType(this, it, ctx)
}
if subResources, ok := e.resourceToSubresourceMap[getResourceKey(ctx.resource)]; ok {
isSubresource := it.IsResource() && it.Resources() != nil && subResources.ContainsAny(it.Resources())
if subResources.Contains(ctx.name) || isSubresource {
it = astmodel.EmptyObjectType // Remove this object
return it, nil
}
}
id, ok := it.Property("Id")
it = it.WithoutProperties()
if ok {
it = it.WithProperties(id)
}
return astmodel.OrderedIdentityVisitOfObjectType(this, it, ctx)
},
}.Build()
return visitor
}
func (e EmbeddedResourceRemover) newResourceRemovalTypeWalker(
visitor astmodel.TypeVisitor[resourceRemovalVisitorContext],
def astmodel.TypeDefinition,
) *astmodel.TypeWalker[resourceRemovalVisitorContext] {
typeWalker := astmodel.NewTypeWalker(e.definitions, visitor)
typeWalker.AfterVisit = func(original astmodel.TypeDefinition, updated astmodel.TypeDefinition, ctx resourceRemovalVisitorContext) (astmodel.TypeDefinition, error) {
if !astmodel.TypeEquals(original.Name(), updated.Name()) {
panic(fmt.Sprintf("Unexpected name mismatch during type walk: %q -> %q", original.Name(), updated.Name()))
}
if astmodel.TypeEquals(original.Type(), updated.Type()) {
return updated, nil
}
flaggedType := e.typeFlag.ApplyTo(updated.Type())
// Generate a unique TypeName for this usage.
// A particular type may be used in multiple contexts in the same resource, or in multiple contexts in different resources. Since the pruning we are
// doing is context specific, a single type may end up with multiple shapes after pruning. In order to cater for this possibility we generate a
// unique name below and then collapse unneeded uniqueness away with simplifyTypeNames.
var newName astmodel.InternalTypeName
var embeddedName embeddedResourceTypeName
exists := false
for count := 0; ; count++ {
embeddedName = embeddedResourceTypeName{
original: original.Name(),
context: ctx.resource.Name(),
suffix: e.typeSuffix,
count: count,
}
newName = embeddedName.ToTypeName()
existing, ok := ctx.modifiedDefinitions[newName]
if !ok {
break
}
if astmodel.TypeEquals(existing.Type(), flaggedType) {
exists = true
// Shape matches what we have already, can proceed
break
}
}
e.renames[newName] = embeddedName
updated = updated.WithName(newName)
updated = updated.WithType(flaggedType)
if !exists {
ctx.modifiedDefinitions.Add(updated)
}
return updated, nil
}
typeWalker.ShouldRemoveCycle = func(def astmodel.TypeDefinition, ctx resourceRemovalVisitorContext) (bool, error) {
ot, ok := astmodel.AsObjectType(def.Type())
if !ok {
return false, nil
}
if ot.IsResource() {
return true, nil
}
// This is here because some microsoft.networking resources are resources (in the sense that they have ARM IDs)
// but can only be created as children of another resource. The resources in question don't have
// their own PUT and so are not actually classified as a top level resource by the JSON schema. We don't want to
// remove ALL cycles in the type graph currently as we can't know for sure that the cycles are structurally meaningless.
// This is an attempt at a middle-ground heuristic that lets us find cycles between things that are resource-like.
// For example see the cycle between NetworkInterfaceIPConfiguration in microsoft.network 20180601:
// NetworkInterfaceIPConfiguration_Status -> NetworkInterfaceIPConfigurationPropertiesFormat_Status ->
// ApplicationGatewayBackendAddressPool_Status -> ApplicationGatewayBackendAddressPoolPropertiesFormat_Status -> NetworkInterfaceIPConfiguration_Status
// Sometimes these resource-like things are promoted to real resources in future APIs as in the case of Subnet in the 2017-06-01
// API version.
if isTypeResourceLookalike(def.Type()) {
return true, nil
}
return false, nil // Leave other cycles for now
}
typeWalker.MakeContext = func(
it astmodel.InternalTypeName,
ctx resourceRemovalVisitorContext,
) (resourceRemovalVisitorContext, error) {
if ctx.resource.IsEmpty() {
return resourceRemovalVisitorContext{
resource: def.Name(),
depth: 0,
modifiedDefinitions: make(astmodel.TypeDefinitionSet),
}, nil
}
return ctx.WithMoreDepth().WithName(it), nil
}
return typeWalker
}
func findResourceSubResources(definitions astmodel.TypeDefinitionSet) map[resourceKey]astmodel.TypeNameSet {
result := make(map[resourceKey]astmodel.TypeNameSet)
for _, def := range definitions.AllResources() {
resource, ok := astmodel.AsResourceType(def.Type())
if !ok {
// Shouldn't be possible to get here
panic(fmt.Sprintf("resource was somehow not a resource: %q", def.Name()))
}
if resource.Owner().IsEmpty() {
continue
}
owner := resource.Owner()
ownerKey := getResourceKey(owner)
if result[ownerKey] == nil {
result[ownerKey] = astmodel.NewTypeNameSet()
}
result[ownerKey].Add(def.Name())
}
return result
}
// requiredResourceProperties are properties that must be on a type for it to be considered a resource
func requiredResourceProperties() []string {
return []string{
"Name",
"Properties",
}
}
func isTypeResourceLookalike(t astmodel.Type) bool {
o, ok := astmodel.AsObjectType(t)
if !ok {
return false
}
return isObjectResourceLookalike(o)
}
func isObjectResourceLookalike(o *astmodel.ObjectType) bool {
hasRequiredProperties := true
for _, propName := range requiredResourceProperties() {
_, hasProp := o.Property(astmodel.PropertyName(propName))
hasRequiredProperties = hasRequiredProperties && hasProp
}
return hasRequiredProperties
}
func findResourcesEmbeddedInParent(
configuration *config.Configuration,
defs astmodel.TypeDefinitionSet,
) (astmodel.TypeAssociation, error) {
result := make(astmodel.TypeAssociation)
var errs []error
for name, def := range defs {
objectType, ok := def.Type().(*astmodel.ObjectType)
if !ok {
continue
}
parentResource, ok := configuration.ObjectModelConfiguration.ResourceEmbeddedInParent.Lookup(name)
if !ok {
// Not configured, nothing to do
continue
}
// Perform some validation that this annotation makes sense before we accept it
if !objectType.IsResource() {
errs = append(errs, eris.Errorf("%s is not labelled as a resource, so cannot be a resource embedded in a parent", name))
continue
}
parentTypeName := name.WithName(parentResource)
if !defs.Contains(parentTypeName) {
err := eris.Errorf("in package %s cannot find %s parent %s", name.InternalPackageReference(), name.Name(), parentTypeName.Name())
errs = append(errs, err)
continue
}
result[name] = parentTypeName
}
var err error
err = kerrors.NewAggregate(errs)
if err != nil {
return nil, err
}
// Ensure that all the $isResource properties were used
err = configuration.ObjectModelConfiguration.ResourceEmbeddedInParent.VerifyConsumed()
if err != nil {
return nil, err
}
return result, nil
}
type resourceKey struct {
name string
group string
}
func getResourceKey(name astmodel.InternalTypeName) resourceKey {
group := name.InternalPackageReference().Group()
return resourceKey{
group: group,
name: name.Name(),
}
}