v2/tools/generator/internal/interfaces/kubernetes_resource_interface.go (517 lines of code) (raw):
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/
package interfaces
import (
"fmt"
"go/token"
"github.com/dave/dst"
"github.com/go-logr/logr"
"github.com/rotisserie/eris"
"github.com/Azure/azure-service-operator/v2/internal/set"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astbuilder"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/astmodel"
"github.com/Azure/azure-service-operator/v2/tools/generator/internal/functions"
)
// AddKubernetesResourceInterfaceImpls adds the required interfaces for
// the resource to be a Kubernetes resource.
// Returns a set of modified definitions.
func AddKubernetesResourceInterfaceImpls(
resourceDef astmodel.TypeDefinition,
idFactory astmodel.IdentifierFactory,
definitions astmodel.TypeDefinitionSet,
log logr.Logger,
) (astmodel.TypeDefinitionSet, error) {
resolved, err := definitions.ResolveResourceSpecAndStatus(resourceDef)
if err != nil {
return nil, eris.Wrapf(err, "unable to resolve resource %s", resourceDef.Name())
}
specDef := resolved.SpecDef
r := resolved.ResourceType
// Check the spec first to ensure it looks how we expect
if r.Scope() == astmodel.ResourceScopeResourceGroup || r.Scope() == astmodel.ResourceScopeExtension {
ownerProperty := idFactory.CreatePropertyName(astmodel.OwnerProperty, astmodel.Exported)
_, ok := resolved.SpecType.Property(ownerProperty)
if !ok {
return nil, eris.Errorf("resource spec doesn't have %q property", ownerProperty)
}
}
azureNameProp, ok := resolved.SpecType.Property(astmodel.AzureNameProperty)
if !ok {
return nil, eris.Errorf("resource spec doesn't have %q property", astmodel.AzureNameProperty)
}
nameFns, err := createAzureNameFunctionHandlersForType(azureNameProp.PropertyType(), definitions, log)
if err != nil {
return nil, err
}
// Sometimes we need to remove the AzureName property from our Spec because the name is forced
if nameFns.removeAzureNameProperty {
// remove the AzureName property from the spec of the resource
remover := astmodel.NewPropertyRemover()
var updated astmodel.TypeDefinition
updated, err = remover.Remove(resolved.SpecDef, astmodel.AzureNameProperty)
if err != nil {
return nil, eris.Wrapf(err, "failed to remove AzureName property from resource %s", resourceDef.Name())
}
specDef = updated
}
getAzureNameProperty := functions.NewObjectFunction(
astmodel.AzureNameProperty,
idFactory,
nameFns.getNameFunction,
astmodel.GenRuntimeReference)
getOwnerProperty := functions.NewResourceFunction(
astmodel.OwnerProperty,
r,
idFactory,
getOwnerFunction,
astmodel.GenRuntimeReference)
getSpecFunction := functions.NewGetSpecFunction(idFactory)
getTypeFunction := functions.NewGetTypeFunction(r.ARMType(), idFactory, functions.ReceiverTypePtr)
getResourceScopeFunc := functions.NewResourceFunction(
"GetResourceScope",
r,
idFactory,
getResourceScopeFunction,
astmodel.GenRuntimeReference)
getSupportedOperationsFunc := functions.NewResourceFunction(
"GetSupportedOperations",
r,
idFactory,
getSupportedOperationsFunction,
astmodel.GenRuntimeReference)
getAPIVersionFunc := functions.NewGetAPIVersionFunction(r.APIVersionEnumValue(), idFactory)
fns := []astmodel.Function{
getAzureNameProperty,
getOwnerProperty,
getSpecFunction,
getTypeFunction,
getResourceScopeFunc,
getSupportedOperationsFunc,
getAPIVersionFunc,
}
if r.StatusType() != nil {
// Skip Status functions if no status
status, ok := astmodel.AsTypeName(r.StatusType())
if !ok {
msg := fmt.Sprintf(
"Unable to create NewEmptyStatus() for resource %s (expected Status to be a TypeName but had %T)",
resourceDef.Name(),
r.StatusType())
return nil, eris.New(msg)
}
emptyStatusFunction := functions.NewEmptyStatusFunction(status, idFactory)
getStatusFunction := functions.NewGetStatusFunction(idFactory)
setStatusFunction := functions.NewResourceStatusSetterFunction(r, idFactory)
fns = append(fns, emptyStatusFunction, getStatusFunction, setStatusFunction)
}
kubernetesResourceImplementation := astmodel.NewInterfaceImplementation(astmodel.KubernetesResourceType, fns...)
interfaceInjector := astmodel.NewInterfaceInjector()
updatedResource, err := interfaceInjector.Inject(resolved.ResourceDef, kubernetesResourceImplementation)
if err != nil {
return nil, eris.Wrapf(err, "failed to inject KubernetesResource interface into resource %s", resourceDef.Name())
}
if nameFns.setNameFunction != nil {
// this function applies to Spec not the resource
functionInjector := astmodel.NewFunctionInjector()
setFn := functions.NewObjectFunction(
astmodel.SetAzureNameFunc,
idFactory,
nameFns.setNameFunction,
astmodel.GenRuntimeReference)
updated, err := functionInjector.Inject(specDef, setFn)
if err != nil {
return nil, eris.Wrapf(err, "failed to inject SetAzureName function into resource %s", resourceDef.Name())
}
specDef = updated
}
result := astmodel.MakeTypeDefinitionSetFromDefinitions(
updatedResource,
specDef,
)
return result, nil
}
func createAzureNameFunctionHandlersForType(
t astmodel.Type,
definitions astmodel.TypeDefinitionSet,
log logr.Logger,
) (createAzureNameFunctionsForTypeResult, error) {
if opt, ok := astmodel.AsOptionalType(t); ok {
t = opt.BaseType()
}
// handle different definitions of AzureName property
switch azureNamePropType := t.(type) {
case *astmodel.ValidatedType:
if !astmodel.TypeEquals(azureNamePropType.ElementType(), astmodel.StringType) {
return createAzureNameFunctionsForTypeResult{},
eris.Errorf("unable to handle non-string validated definitions in AzureName property")
}
validations := azureNamePropType.Validations().(astmodel.StringValidations)
if len(validations.Patterns) != 0 {
if len(validations.Patterns) == 1 &&
validations.Patterns[0].String() == "^.*/default$" {
// Validation requires the resource be named exactly "default"
return createAzureNameFunctionsForTypeResult{
getNameFunction: fixedValueGetAzureNameFunction("default"),
removeAzureNameProperty: true,
}, nil
}
// ignoring for now:
log.V(1).Info("ignoring pattern validation on Name property", "pattern", validations.Patterns[0].String())
return createAzureNameFunctionsForTypeResult{
getNameFunction: getStringAzureNameFunction,
setNameFunction: setStringAzureNameFunction,
}, nil
}
// ignoring length validations for now
// return nil, errors.Errorf("unable to handle validations on Name property …TODO")
return createAzureNameFunctionsForTypeResult{
getNameFunction: getStringAzureNameFunction,
setNameFunction: setStringAzureNameFunction,
}, nil
case astmodel.TypeName:
// resolve property type if it is a typename
resolvedPropType, err := definitions.FullyResolve(azureNamePropType)
if err != nil {
return createAzureNameFunctionsForTypeResult{},
eris.Wrapf(err, "unable to resolve type of resource Name property: %s", azureNamePropType.String())
}
if t, ok := resolvedPropType.(*astmodel.EnumType); ok {
if !astmodel.TypeEquals(t.BaseType(), astmodel.StringType) {
return createAzureNameFunctionsForTypeResult{},
eris.Errorf("unable to handle non-string enum base type in Name property")
}
options := t.Options()
if len(options) == 1 {
// if there is only one possible value,
// we make an AzureName function that returns it, and do not
// provide an AzureName property on the spec
return createAzureNameFunctionsForTypeResult{
getNameFunction: fixedValueGetAzureNameFunction(options[0].Value),
removeAzureNameProperty: true,
}, nil
}
// with multiple values, provide an AzureName function that casts from the
// enum-valued AzureName property:
return createAzureNameFunctionsForTypeResult{
getNameFunction: getEnumAzureNameFunction(azureNamePropType),
setNameFunction: setEnumAzureNameFunction(azureNamePropType),
}, nil
}
return createAzureNameFunctionsForTypeResult{},
eris.Errorf("unable to produce AzureName()/SetAzureName() for Name property with type %s", resolvedPropType.String())
case *astmodel.PrimitiveType:
if !astmodel.TypeEquals(azureNamePropType, astmodel.StringType) {
return createAzureNameFunctionsForTypeResult{},
eris.Errorf("cannot use type %s as type of AzureName property", azureNamePropType.String())
}
return createAzureNameFunctionsForTypeResult{
getNameFunction: getStringAzureNameFunction,
setNameFunction: setStringAzureNameFunction,
}, nil
default:
return createAzureNameFunctionsForTypeResult{},
eris.Errorf("unsupported type for AzureName property: %s", azureNamePropType.String())
}
}
type createAzureNameFunctionsForTypeResult struct {
getNameFunction functions.ObjectFunctionHandler
setNameFunction functions.ObjectFunctionHandler
removeAzureNameProperty bool
}
// getEnumAzureNameFunction adds an AzureName() function that casts the AzureName property
// with an enum value to a string
func getEnumAzureNameFunction(enumType astmodel.TypeName) functions.ObjectFunctionHandler {
return func(
f *functions.ObjectFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := f.IDFactory().CreateReceiver(receiver.Name())
receiverExpr, err := receiver.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: astbuilder.PointerTo(receiverExpr),
Body: astbuilder.Statements(
astbuilder.Returns(
astbuilder.CallFunc("string", astbuilder.Selector(dst.NewIdent(receiverIdent), "Spec", astmodel.AzureNameProperty)))),
}
fn.AddComments(fmt.Sprintf("returns the Azure name of the resource (string representation of %s)", enumType.String()))
fn.AddReturns("string")
return fn.DefineFunc(), nil
}
}
// setEnumAzureNameFunction returns a function that sets the AzureName property to the result of casting
// the argument string to the given enum type
func setEnumAzureNameFunction(enumType astmodel.TypeName) functions.ObjectFunctionHandler {
return func(
f *functions.ObjectFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := f.IDFactory().CreateReceiver(receiver.Name())
receiverTypeExpr, err := receiver.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
azureNameProp := astbuilder.Selector(dst.NewIdent(receiverIdent), astmodel.AzureNameProperty)
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: astbuilder.PointerTo(receiverTypeExpr),
Body: astbuilder.Statements(
astbuilder.SimpleAssignment(
azureNameProp,
astbuilder.CallFunc(enumType.Name(), dst.NewIdent("azureName")))),
}
fn.AddComments(fmt.Sprintf("sets the Azure name from the given %s value", enumType.String()))
fn.AddParameter("azureName", dst.NewIdent("string"))
return fn.DefineFunc(), nil
}
}
// fixedValueGetAzureNameFunction adds an AzureName() function that returns a fixed value
func fixedValueGetAzureNameFunction(fixedValue string) functions.ObjectFunctionHandler {
// ensure fixedValue is quoted. This is always the case with enum values we pass,
// but let's be safe:
if len(fixedValue) == 0 {
panic("cannot created fixed value AzureName function with empty fixed value")
}
if fixedValue[0] != '"' || fixedValue[len(fixedValue)-1] != '"' {
fixedValue = fmt.Sprintf("%q", fixedValue)
}
return func(
f *functions.ObjectFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := f.IDFactory().CreateReceiver(receiver.Name())
receiverExpr, err := receiver.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: astbuilder.PointerTo(receiverExpr),
Body: astbuilder.Statements(
astbuilder.Returns(
astbuilder.TextLiteral(fixedValue))),
}
fn.AddComments(fmt.Sprintf("returns the Azure name of the resource (always %s)", fixedValue))
fn.AddReturns("string")
return fn.DefineFunc(), nil
}
}
// getOwnerFunction creates the Owner function declaration. This has two possible formats.
// For normal resources:
//
// func (<receiver> *<receiver>) Owner() *genruntime.ResourceReference {
// group, kind := genruntime.LookupOwnerGroupKind(<receiver>.Spec)
// return <receiver>.Spec.Owner.AsKnownResourceReference(group, kind)
// }
//
// For extension resources:
//
// func (<receiver> *<receiver>) Owner() *genruntime.ResourceReference {
// return <receiver>.Spec.Owner.AsKnownResourceReference()
// }
func getOwnerFunction(
r *functions.ResourceFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := r.IDFactory().CreateReceiver(receiver.Name())
receiverType := astmodel.NewOptionalType(receiver)
receiverTypeExpr, err := receiverType.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
specSelector := astbuilder.Selector(dst.NewIdent(receiverIdent), "Spec")
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: receiverTypeExpr,
Params: nil,
}
resourceReferenceTypeExpr, err := astmodel.ResourceReferenceType.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating resource reference type expression")
}
fn.AddReturn(astbuilder.Dereference(resourceReferenceTypeExpr))
groupLocal := "group"
kindLocal := "kind"
if receiverIdent == groupLocal || receiverIdent == kindLocal {
groupLocal = "ownerGroup"
kindLocal = "ownerKind"
}
owner := astbuilder.Selector(specSelector, astmodel.OwnerProperty)
nilOwnerCheck := astbuilder.IfNil(owner, astbuilder.Returns(astbuilder.Nil()))
nilOwnerCheck.Decs.After = dst.EmptyLine
switch r.Resource().Scope() {
case astmodel.ResourceScopeResourceGroup:
fn.AddComments("returns the ResourceReference of the owner")
fn.AddStatements(
nilOwnerCheck,
lookupGroupAndKindStmt(groupLocal, kindLocal, specSelector),
astbuilder.Returns(createResourceReferenceWithGroupKind(dst.NewIdent(groupLocal), dst.NewIdent(kindLocal), owner)))
case astmodel.ResourceScopeExtension:
fn.AddComments("returns the ResourceReference of the owner")
fn.AddStatements(
nilOwnerCheck,
astbuilder.Returns(createResourceReference(owner)))
case astmodel.ResourceScopeTenant:
// Tenant resources never have an owner, just return nil
fn.AddComments("returns nil as Tenant scoped resources never have an owner")
fn.AddStatements(astbuilder.Returns(astbuilder.Nil()))
case astmodel.ResourceScopeLocation:
// Location resources never have an owner, just return nil
fn.AddComments("returns nil as Location scoped resources never have an owner")
fn.AddStatements(astbuilder.Returns(astbuilder.Nil()))
default:
panic(fmt.Sprintf("unknown resource kind: %s", r.Resource().Scope()))
}
return fn.DefineFunc(), nil
}
// getResourceScopeFunction creates a function that returns the scope of the resource.
//
// func (<receiver> *<receiver>) GetResourceScope() genruntime.ResourceScope {
// return genruntime.ResourceScopeResourceGroup
// }
func getResourceScopeFunction(
r *functions.ResourceFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := r.IDFactory().CreateReceiver(receiver.Name())
receiverType := astmodel.NewOptionalType(receiver)
receiverTypeExpr, err := receiverType.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
var resourceScope string
switch r.Resource().Scope() {
case astmodel.ResourceScopeLocation:
resourceScope = "ResourceScopeLocation"
case astmodel.ResourceScopeResourceGroup:
resourceScope = "ResourceScopeResourceGroup"
case astmodel.ResourceScopeExtension:
resourceScope = "ResourceScopeExtension"
case astmodel.ResourceScopeTenant:
resourceScope = "ResourceScopeTenant"
default:
panic(fmt.Sprintf("unknown resource scope %s", r.Resource().Scope()))
}
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: receiverTypeExpr,
Params: nil,
Body: astbuilder.Statements(
astbuilder.Returns(
astbuilder.Selector(dst.NewIdent(astmodel.GenRuntimeReference.PackageName()), resourceScope))),
}
fn.AddComments("returns the scope of the resource")
resourceScopeTypeExpr, err := astmodel.ResourceScopeType.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating resource scope type expression")
}
fn.AddReturn(resourceScopeTypeExpr)
return fn.DefineFunc(), nil
}
// getSupportedOperationsFunction creates a function that returns the supported operations of the resource.
//
// func (<receiver> *<receiver>) GetSupportedOperations() []genruntime.ResourceOperation {
// return []genruntime.ResourceOperation{
// genruntime.ResourceOperationGet,
// genruntime.ResourceOperationPut,
// genruntime.ResourceOperationDelete,
// }
// }
func getSupportedOperationsFunction(
r *functions.ResourceFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := r.IDFactory().CreateReceiver(receiver.Name())
receiverType := astmodel.NewOptionalType(receiver)
receiverTypeExpr, err := receiverType.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
genruntimePackage := codeGenerationContext.MustGetImportedPackageName(astmodel.GenRuntimeReference)
supportedOperations := set.AsSortedSlice(r.Resource().SupportedOperations()) // Sorted to ensure ordered codegen
idents := make([]dst.Expr, 0, len(supportedOperations))
for _, op := range supportedOperations {
var genruntimeOpName string
switch op {
case astmodel.ResourceOperationPut:
genruntimeOpName = "ResourceOperationPut"
case astmodel.ResourceOperationGet:
genruntimeOpName = "ResourceOperationGet"
case astmodel.ResourceOperationHead:
genruntimeOpName = "ResourceOperationHead"
case astmodel.ResourceOperationDelete:
genruntimeOpName = "ResourceOperationDelete"
default:
panic(fmt.Sprintf("unknown resource operation %s", op))
}
idents = append(idents, astbuilder.Selector(dst.NewIdent(genruntimePackage), genruntimeOpName))
}
resourceOperationTypeExpr, err := astmodel.ResourceOperationType.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating resource operation type expression")
}
sliceBuilder := astbuilder.NewSliceLiteralBuilder(resourceOperationTypeExpr, true)
for _, id := range idents {
sliceBuilder.AddElement(id)
}
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: receiverTypeExpr,
Params: nil,
Body: astbuilder.Statements(
astbuilder.Returns(sliceBuilder.Build()),
),
}
fn.AddComments("returns the operations supported by the resource")
resourceOperationTypeArrayExpr, err := astmodel.ResourceOperationTypeArray.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating resource operation type array expression")
}
fn.AddReturn(resourceOperationTypeArrayExpr)
return fn.DefineFunc(), nil
}
func lookupGroupAndKindStmt(
groupIdent string,
kindIdent string,
specSelector *dst.SelectorExpr,
) *dst.AssignStmt {
return &dst.AssignStmt{
Lhs: []dst.Expr{
dst.NewIdent(groupIdent),
dst.NewIdent(kindIdent),
},
Tok: token.DEFINE,
Rhs: []dst.Expr{
astbuilder.CallExpr(
dst.NewIdent(astmodel.GenRuntimeReference.PackageName()),
"LookupOwnerGroupKind",
specSelector),
},
}
}
func createResourceReferenceWithGroupKind(
group dst.Expr,
kind dst.Expr,
ownerSelector dst.Expr,
) dst.Expr {
return astbuilder.CallExpr(ownerSelector, "AsResourceReference", group, kind)
}
func createResourceReference(
ownerSelector dst.Expr,
) dst.Expr {
return astbuilder.CallExpr(ownerSelector, "AsResourceReference")
}
// setStringAzureNameFunction returns a function that sets the Name property of
// the resource spec to the argument string
func setStringAzureNameFunction(
k *functions.ObjectFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := k.IDFactory().CreateReceiver(receiver.Name())
receiverTypeExpr, err := receiver.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: astbuilder.PointerTo(receiverTypeExpr),
Body: astbuilder.Statements(
astbuilder.QualifiedAssignment(
dst.NewIdent(receiverIdent),
astmodel.AzureNameProperty,
token.ASSIGN,
dst.NewIdent("azureName")),
),
}
fn.AddComments("sets the Azure name of the resource")
fn.AddParameter("azureName", dst.NewIdent("string"))
return fn.DefineFunc(), nil
}
// getStringAzureNameFunction returns a function that returns the Name property of the resource spec
func getStringAzureNameFunction(
k *functions.ObjectFunction,
codeGenerationContext *astmodel.CodeGenerationContext,
receiver astmodel.TypeName,
methodName string,
) (*dst.FuncDecl, error) {
receiverIdent := k.IDFactory().CreateReceiver(receiver.Name())
receiverTypeExpr, err := receiver.AsTypeExpr(codeGenerationContext)
if err != nil {
return nil, eris.Wrap(err, "creating receiver type expression")
}
fn := &astbuilder.FuncDetails{
Name: methodName,
ReceiverIdent: receiverIdent,
ReceiverType: astbuilder.PointerTo(receiverTypeExpr),
Body: astbuilder.Statements(
astbuilder.Returns(
astbuilder.Selector(astbuilder.Selector(dst.NewIdent(receiverIdent), "Spec"), astmodel.AzureNameProperty))),
}
fn.AddComments("returns the Azure name of the resource")
fn.AddReturns("string")
return fn.DefineFunc(), nil
}