Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: PoC - Support configuration of timeout in schema #2717

Merged
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
wip
AgustinBettati committed Oct 18, 2024
commit 3909c505902cd10387f409d8f01b3f713197fe30
32 changes: 32 additions & 0 deletions tools/codegen/codespec/api_to_provider_spec_mapper.go
Original file line number Diff line number Diff line change
@@ -221,3 +221,35 @@ func extractCommonParameters(paths *high.Paths, path string) ([]*high.Parameter,

return pathItem.Parameters, nil
}

func applyConfigSchemaOptions(resourceConfig config.Resource, resource *Resource) {
parseTimeoutConfig(resourceConfig, resource)
}

func parseTimeoutConfig(config config.Resource, resource *Resource) {
var result []Operation
for _, op := range config.SchemaOptions.Timeouts {
switch op {
case "create":
result = append(result, Create)
case "read":
result = append(result, Read)
case "delete":
result = append(result, Delete)
case "update":
result = append(result, Update)
default:
log.Printf("[WARN] Unknown operation type defined in timeout configuration: %s", op)
}
}
if result != nil {
existing := resource.Schema.Attributes
resource.Schema.Attributes = append(existing,
Attribute{
Name: "timeouts",
Timeouts: &TimeoutsAttribute{
ConfigurableTimeouts: result},
},
)
}
}
6 changes: 6 additions & 0 deletions tools/codegen/codespec/api_to_provider_spec_mapper_test.go
Original file line number Diff line number Diff line change
@@ -87,6 +87,12 @@ func TestConvertToProviderSpec(t *testing.T) {
ComputedOptionalRequired: codespec.Required,
Description: conversion.StringPtr(testFieldDesc),
},
{
Name: "timeouts",
Timeouts: &codespec.TimeoutsAttribute{
ConfigurableTimeouts: []codespec.Operation{codespec.Create, codespec.Read, codespec.Delete},
},
},
},
},
Name: "test_resource",
14 changes: 14 additions & 0 deletions tools/codegen/codespec/model.go
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ type Attribute struct {
Set *SetAttribute
Int64 *Int64Attribute
SingleNested *SingleNestedAttribute
Timeouts *TimeoutsAttribute
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be on a resource level (in Schema might be more intuitive). Any reason we want to keep it at an attribute level? Is it only because it simplifies code generation?

Copy link
Member Author

@AgustinBettati AgustinBettati Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was thinking at the attribute level given how it is configured in terraform plugin framework schema. You will see it is configure as an attribute, not as a property at the schema level. Defining at the resource is definitely possible, but think an attribute type is more aligned with how it is defined.


Description *string
Name SnakeCaseString
@@ -99,6 +100,19 @@ type NestedAttributeObject struct {
Attributes Attributes
}

type TimeoutsAttribute struct {
ConfigurableTimeouts []Operation
}

type Operation int

const (
Create Operation = iota
Update
Read
Delete
)

type CustomDefault struct {
Definition string
Imports []string
5 changes: 4 additions & 1 deletion tools/codegen/codespec/testdata/config-no-schema-opts.yml
Original file line number Diff line number Diff line change
@@ -11,4 +11,7 @@ resources:
method: PATCH
delete:
path: /api/atlas/v2/groups/{groupId}/testResource
method: DELETE
method: DELETE

schema:
timeouts: ["create", "read", "delete"]
2 changes: 1 addition & 1 deletion tools/codegen/codespec/testdata/config.yml
Original file line number Diff line number Diff line change
@@ -33,4 +33,4 @@ resources:
optional: true
computed: true

timeouts: ["create", "read", "delete"]
timeouts: ["create", "read", "delete"]
118 changes: 95 additions & 23 deletions tools/codegen/schema/schema_attribute.go
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ func GenerateSchemaAttributes(attrs codespec.Attributes) CodeStatement {
attrsCode := []string{}
imports := []string{}
for i := range attrs {
result := generateAttribute(&attrs[i])
result := generator(&attrs[i]).AttributeCode()
attrsCode = append(attrsCode, result.Code)
imports = append(imports, result.Imports...)
}
@@ -22,20 +22,53 @@ func GenerateSchemaAttributes(attrs codespec.Attributes) CodeStatement {
}
}

func generateAttribute(attr *codespec.Attribute) CodeStatement {
generator := typeGenerator(attr)
type attributeGenerator interface {
AttributeCode() CodeStatement
}

type TimeoutAttributeGenerator struct {
timeouts codespec.TimeoutsAttribute
}

func (s *TimeoutAttributeGenerator) AttributeCode() CodeStatement {
var optionProperties string
for op := range s.timeouts.ConfigurableTimeouts {
switch op {
case int(codespec.Create):
optionProperties += "Create: true,"
case int(codespec.Update):
optionProperties += "Update: true,"
case int(codespec.Delete):
optionProperties += "Delete: true,"
case int(codespec.Read):
optionProperties += "Read: true,"
}
}
return CodeStatement{
Code: fmt.Sprintf(`"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
%s
})`, optionProperties),
Imports: []string{"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"},
}
}

type ConventionalAttributeGenerator struct {
typeSpecificCode convetionalTypeSpecificCodeGenerator
attribute codespec.Attribute
}

typeDefinition := generator.TypeDefinition()
additionalPropertyStatements := generator.TypeSpecificProperties()
func (s *ConventionalAttributeGenerator) AttributeCode() CodeStatement {
typeDefinition := s.typeSpecificCode.TypeDefinition()
additionalPropertyStatements := s.typeSpecificCode.TypeSpecificProperties()

properties := commonProperties(attr)
imports := []string{"github.com/hashicorp/terraform-plugin-framework/resource/schema"}
properties := commonProperties(&s.attribute)
imports := []string{}
for i := range additionalPropertyStatements {
properties = append(properties, additionalPropertyStatements[i].Code)
imports = append(imports, additionalPropertyStatements[i].Imports...)
}

name := attr.Name
name := s.attribute.Name
propsResultString := strings.Join(properties, ",\n") + ","
code := fmt.Sprintf(`"%s": %s{
%s
@@ -66,47 +99,86 @@ func commonProperties(attr *codespec.Attribute) []string {
return result
}

type schemaAttributeGenerator interface {
type convetionalTypeSpecificCodeGenerator interface {
TypeDefinition() string
TypeSpecificProperties() []CodeStatement
}

func typeGenerator(attr *codespec.Attribute) schemaAttributeGenerator {
func generator(attr *codespec.Attribute) attributeGenerator {
if attr.Int64 != nil {
return &Int64AttrGenerator{model: *attr.Int64}
return &ConventionalAttributeGenerator{
typeSpecificCode: &Int64AttrGenerator{model: *attr.Int64},
attribute: *attr,
}
}
if attr.Float64 != nil {
return &Float64AttrGenerator{model: *attr.Float64}
return &ConventionalAttributeGenerator{
typeSpecificCode: &Float64AttrGenerator{model: *attr.Float64},
attribute: *attr,
}
}
if attr.String != nil {
return &StringAttrGenerator{model: *attr.String}
return &ConventionalAttributeGenerator{
typeSpecificCode: &StringAttrGenerator{model: *attr.String},
attribute: *attr,
}
}
if attr.Bool != nil {
return &BoolAttrGenerator{model: *attr.Bool}
return &ConventionalAttributeGenerator{
typeSpecificCode: &BoolAttrGenerator{model: *attr.Bool},
attribute: *attr,
}
}
if attr.List != nil {
return &ListAttrGenerator{model: *attr.List}
return &ConventionalAttributeGenerator{
typeSpecificCode: &ListAttrGenerator{model: *attr.List},
attribute: *attr,
}
}
if attr.ListNested != nil {
return &ListNestedAttrGenerator{model: *attr.ListNested}
return &ConventionalAttributeGenerator{
typeSpecificCode: &ListNestedAttrGenerator{model: *attr.ListNested},
attribute: *attr,
}
}
if attr.Map != nil {
return &MapAttrGenerator{model: *attr.Map}
return &ConventionalAttributeGenerator{
typeSpecificCode: &MapAttrGenerator{model: *attr.Map},
attribute: *attr,
}
}
if attr.MapNested != nil {
return &MapNestedAttrGenerator{model: *attr.MapNested}
return &ConventionalAttributeGenerator{
typeSpecificCode: &MapNestedAttrGenerator{model: *attr.MapNested},
attribute: *attr,
}
}
if attr.Number != nil {
return &NumberAttrGenerator{model: *attr.Number}
return &ConventionalAttributeGenerator{
typeSpecificCode: &NumberAttrGenerator{model: *attr.Number},
attribute: *attr,
}
}
if attr.Set != nil {
return &SetAttrGenerator{model: *attr.Set}
return &ConventionalAttributeGenerator{
typeSpecificCode: &SetAttrGenerator{model: *attr.Set},
attribute: *attr,
}
}
if attr.SetNested != nil {
return &SetNestedGenerator{model: *attr.SetNested}
return &ConventionalAttributeGenerator{
typeSpecificCode: &SetNestedGenerator{model: *attr.SetNested},
attribute: *attr,
}
}
if attr.SingleNested != nil {
return &SingleNestedAttrGenerator{model: *attr.SingleNested}
return &ConventionalAttributeGenerator{
typeSpecificCode: &SingleNestedAttrGenerator{model: *attr.SingleNested},
attribute: *attr,
}
}
if attr.Timeouts != nil {
return &TimeoutAttributeGenerator{}
}
panic("Attribute with unknown type defined")
panic("Attribute with unknown type defined when generating schema attribute")
}
6 changes: 5 additions & 1 deletion tools/codegen/schema/schema_file.go
Original file line number Diff line number Diff line change
@@ -11,9 +11,13 @@ func GenerateGoCode(input codespec.Resource) string {
schemaAttrs := GenerateSchemaAttributes(input.Schema.Attributes)
models := GenerateTypedModels(input.Schema.Attributes)

imports := []string{"github.com/hashicorp/terraform-plugin-framework/resource/schema"}
imports = append(imports, schemaAttrs.Imports...)
imports = append(imports, models.Imports...)

tmplInputs := codetemplate.SchemaFileInputs{
PackageName: input.Name.LowerCaseNoUnderscore(),
Imports: append(schemaAttrs.Imports, models.Imports...),
Imports: imports,
SchemaAttributes: schemaAttrs.Code,
Models: models.Code,
}
16 changes: 16 additions & 0 deletions tools/codegen/schema/schema_file_test.go
Original file line number Diff line number Diff line change
@@ -144,6 +144,22 @@ func TestSchemaGenerationFromCodeSpec(t *testing.T) {
},
goldenFileName: "nested-attributes",
},
"timeout attribute": {
inputModel: codespec.Resource{
Name: "test_name",
Schema: &codespec.Schema{
Attributes: []codespec.Attribute{
{
Name: "timeouts",
Timeouts: &codespec.TimeoutsAttribute{
ConfigurableTimeouts: []codespec.Operation{codespec.Create, codespec.Read, codespec.Delete},
},
},
},
},
},
goldenFileName: "timeouts",
},
}

for testName, tc := range testCases {
20 changes: 20 additions & 0 deletions tools/codegen/schema/testdata/timeouts.golden.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package test_name

import (
"context"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)

func ResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{}),
},
}
}

type TFModel struct {
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
4 changes: 3 additions & 1 deletion tools/codegen/schema/typed_model.go
Original file line number Diff line number Diff line change
@@ -97,6 +97,8 @@ func attrModelType(attr *codespec.Attribute) string {
return "types.Number"
case attr.Int64 != nil:
return "types.Int64"
case attr.Timeouts != nil:
return "timeouts.Value" // TODO add import statement
// For nested attributes the generic type is used, this is to ensure the model can store all possible values. e.g. nested computed only attributes need to be defined with TPF types to avoid error when getting the config.
case attr.List != nil || attr.ListNested != nil:
return "types.List"
@@ -107,6 +109,6 @@ func attrModelType(attr *codespec.Attribute) string {
case attr.SingleNested != nil:
return "types.Object"
default:
panic("Attribute with unknown type defined")
panic("Attribute with unknown type defined when generating typed model")
}
}