From 2796d0edc679ce3ee6a57d2556e41407cd260bab Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Mon, 12 Aug 2024 13:28:21 +0100 Subject: [PATCH] initial pass of default_organization support --- examples/provider/provider.tf | 1 + internal/provider/inventory_resource.go | 23 ++-- internal/provider/inventory_resource_test.go | 14 +- internal/provider/provider.go | 61 +++++++-- internal/provider/provider_test.go | 136 +++++++++++-------- 5 files changed, 152 insertions(+), 83 deletions(-) diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index e010e49..c21a5bf 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -13,6 +13,7 @@ provider "aap" { host = "https://localhost:8043" username = "ansible" password = "test123!" + default_orgaization = 4 insecure_skip_verify = true } diff --git a/internal/provider/inventory_resource.go b/internal/provider/inventory_resource.go index 4a12284..d04c30c 100644 --- a/internal/provider/inventory_resource.go +++ b/internal/provider/inventory_resource.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "log" "path" "github.com/ansible/terraform-provider-aap/internal/provider/customtypes" @@ -24,13 +25,16 @@ var ( ) // NewInventoryResource is a helper function to simplify the provider implementation. -func NewInventoryResource() resource.Resource { - return &InventoryResource{} +func NewInventoryResource(providerModel *aapProviderModel) resource.Resource { + return &InventoryResource{ + providerModel: providerModel, + } } // InventoryResource is the resource implementation. type InventoryResource struct { - client ProviderHTTPClient + providerModel *aapProviderModel + client ProviderHTTPClient } // Metadata returns the resource type name. @@ -75,7 +79,7 @@ func (r *InventoryResource) Schema(_ context.Context, _ resource.SchemaRequest, int64planmodifier.UseStateForUnknown(), }, Description: "Identifier for the organization the inventory should be created in. " + - "If not provided, the inventory will be created in the default organization.", + "If not specified on the resource or in the provider block, the inventory will be created in the default organization.", }, "url": schema.StringAttribute{ Computed: true, @@ -115,7 +119,7 @@ func (r *InventoryResource) Create(ctx context.Context, req resource.CreateReque } // Generate request body from inventory data - createRequestBody, diags := data.generateRequestBody() + createRequestBody, diags := data.generateRequestBody(*r.providerModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -192,7 +196,7 @@ func (r *InventoryResource) Update(ctx context.Context, req resource.UpdateReque } // Generate request body from inventory data - updateRequestBody, diags := data.generateRequestBody() + updateRequestBody, diags := data.generateRequestBody(*r.providerModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -252,15 +256,17 @@ type inventoryResourceModel struct { } // generateRequestBody creates a JSON encoded request body from the inventory resource data. -func (r *inventoryResourceModel) generateRequestBody() ([]byte, diag.Diagnostics) { +func (r *inventoryResourceModel) generateRequestBody(providerModel aapProviderModel) ([]byte, diag.Diagnostics) { // Convert inventory resource data to API data model var organizationId int64 // Use default organization if not provided if r.Organization.ValueInt64() == 0 { - organizationId = 1 + organizationId = providerModel.DefaultOrganization.ValueInt64() + log.Printf("Organization ID set to the Default: %d", organizationId) } else { organizationId = r.Organization.ValueInt64() + log.Printf("Organization ID set by the Resource: %d", organizationId) } inventory := InventoryAPIModel{ Organization: organizationId, @@ -279,7 +285,6 @@ func (r *inventoryResourceModel) generateRequestBody() ([]byte, diag.Diagnostics ) return nil, diags } - return jsonBody, nil } diff --git a/internal/provider/inventory_resource_test.go b/internal/provider/inventory_resource_test.go index 77c2668..9b283d7 100644 --- a/internal/provider/inventory_resource_test.go +++ b/internal/provider/inventory_resource_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -26,7 +27,7 @@ func TestInventoryResourceSchema(t *testing.T) { schemaResponse := &fwresource.SchemaResponse{} // Instantiate the InventoryResource and call its Schema method - NewInventoryResource().Schema(ctx, schemaRequest, schemaResponse) + NewInventoryResource(&aapProviderModel{}).Schema(ctx, schemaRequest, schemaResponse) if schemaResponse.Diagnostics.HasError() { t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) @@ -89,7 +90,16 @@ func TestInventoryResourceGenerateRequestBody(t *testing.T) { for _, test := range testTable { t.Run(test.name, func(t *testing.T) { - actual, diags := test.input.generateRequestBody() + providerModel := aapProviderModel{ + Host: basetypes.StringValue{}, + Username: basetypes.StringValue{}, + Password: basetypes.StringValue{}, + DefaultOrganization: types.Int64Value(1), + InsecureSkipVerify: basetypes.BoolValue{}, + Timeout: basetypes.Int64Value{}, + } + + actual, diags := test.input.generateRequestBody(providerModel) if diags.HasError() { t.Fatal(diags.Errors()) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 88dbfb6..a5d64bb 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -34,6 +34,7 @@ type aapProvider struct { // provider is built and ran locally, and "test" when running acceptance // testing. version string + config aapProviderModel } // Metadata returns the provider type name. @@ -56,12 +57,17 @@ func (p *aapProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp * Optional: true, Sensitive: true, }, + "default_organization": schema.Int64Attribute{ + Optional: true, + Description: "Default organization ID to use for resources that require an organization ID. " + + "Defaults to 1 (the Default Organization) if not provided.", + }, "insecure_skip_verify": schema.BoolAttribute{ Optional: true, }, "timeout": schema.Int64Attribute{ Optional: true, - Description: "Timeout specifies a time limit for requests made to the AAP server." + + Description: "Timeout specifies a time limit for requests made to the AAP server. " + "Defaults to 5 if not provided. A Timeout of zero means no timeout.", }, }, @@ -107,18 +113,18 @@ func (p *aapProvider) Configure(ctx context.Context, req provider.ConfigureReque var host, username, password string var insecureSkipVerify bool var timeout int64 - config.ReadValues(&host, &username, &password, &insecureSkipVerify, &timeout, resp) + var default_organization int64 + config.ReadValues(&host, &username, &password, &default_organization, &insecureSkipVerify, &timeout, resp) if resp.Diagnostics.HasError() { return } - + // Explicitly set the DefaultOrganization to the value read from the configuration + config.DefaultOrganization = types.Int64Value(default_organization) // If any of the expected configurations are missing, return // errors with provider-specific guidance. - if len(host) == 0 { AddConfigurationAttributeError(resp, "host", "AAP_HOST", false) } - if len(username) == 0 { AddConfigurationAttributeError(resp, "username", "AAP_USERNAME", false) } @@ -139,6 +145,8 @@ func (p *aapProvider) Configure(ctx context.Context, req provider.ConfigureReque // type Configure methods. resp.DataSourceData = client resp.ResourceData = client + // make the config available through the provider + p.config = config } // DataSources defines the data sources implemented in the provider. @@ -148,10 +156,17 @@ func (p *aapProvider) DataSources(_ context.Context) []func() datasource.DataSou } } +// Wrapper function to adapt NewInventoryResource to the expected signature and be able to pass the default org around +func NewInventoryResourceWrapper(providerModel *aapProviderModel) func() resource.Resource { + return func() resource.Resource { + return NewInventoryResource(providerModel) + } +} + // Resources defines the resources implemented in the provider. func (p *aapProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ - NewInventoryResource, + NewInventoryResourceWrapper(&p.config), NewJobResource, NewGroupResource, NewHostResource, @@ -160,11 +175,12 @@ func (p *aapProvider) Resources(_ context.Context) []func() resource.Resource { // aapProviderModel maps provider schema data to a Go type. type aapProviderModel struct { - Host types.String `tfsdk:"host"` - Username types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` - InsecureSkipVerify types.Bool `tfsdk:"insecure_skip_verify"` - Timeout types.Int64 `tfsdk:"timeout"` + Host types.String `tfsdk:"host"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + DefaultOrganization types.Int64 `tfsdk:"default_organization"` + InsecureSkipVerify types.Bool `tfsdk:"insecure_skip_verify"` + Timeout types.Int64 `tfsdk:"timeout"` } func (p *aapProviderModel) checkUnknownValue(resp *provider.ConfigureResponse) { @@ -180,6 +196,10 @@ func (p *aapProviderModel) checkUnknownValue(resp *provider.ConfigureResponse) { AddConfigurationAttributeError(resp, "password", "AAP_PASSWORD", true) } + if p.DefaultOrganization.IsUnknown() { + AddConfigurationAttributeError(resp, "default_organization", "AAP_DEFAULT_ORGANIZATION", true) + } + if p.InsecureSkipVerify.IsUnknown() { AddConfigurationAttributeError(resp, "insecure_skip_verify", "AAP_INSECURE_SKIP_VERIFY", true) } @@ -192,9 +212,10 @@ func (p *aapProviderModel) checkUnknownValue(resp *provider.ConfigureResponse) { const ( DefaultTimeOut = 5 // Default http session timeout DefaultInsecureSkipVerify = false // Default value for insecure skip verify + DefaultOrganization = 1 // Default organization ID ) -func (p *aapProviderModel) ReadValues(host, username, password *string, insecureSkipVerify *bool, +func (p *aapProviderModel) ReadValues(host, username, password *string, defaultOrganization *int64, insecureSkipVerify *bool, timeout *int64, resp *provider.ConfigureResponse) { // Set default values from env variables *host = os.Getenv("AAP_HOST") @@ -217,6 +238,22 @@ func (p *aapProviderModel) ReadValues(host, username, password *string, insecure *password = p.Password.ValueString() } + // setting default organization value + *defaultOrganization = DefaultOrganization + if !p.DefaultOrganization.IsNull() { + *defaultOrganization = p.DefaultOrganization.ValueInt64() + } else if intValue := os.Getenv("AAP_DEFAULT_ORGANIZATION"); intValue != "" { + // convert string into int64 value + *defaultOrganization, err = strconv.ParseInt(intValue, 10, 64) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("default_organization"), + "Invalid value for default_organization", + "The provider cannot create the AAP API client as the value provided for default_organization is not a valid int64 value.", + ) + } + } + if !p.InsecureSkipVerify.IsNull() { *insecureSkipVerify = p.InsecureSkipVerify.ValueBool() } else if boolValue := os.Getenv("AAP_INSECURE_SKIP_VERIFY"); boolValue != "" { diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 0302a6b..b2c1a21 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -61,26 +61,28 @@ func testGetResource(urlPath string) ([]byte, error) { func TestReadValues(t *testing.T) { testTable := []struct { - name string - config aapProviderModel - envVars map[string]string - Host string - Username string - Password string - InsecureSkipVerify bool - Timeout int64 - Errors int + name string + config aapProviderModel + envVars map[string]string + Host string + Username string + Password string + DefaultOrganization int64 + InsecureSkipVerify bool + Timeout int64 + Errors int }{ { - name: "No defined values", - config: aapProviderModel{}, - envVars: map[string]string{}, - Host: "", - Username: "", - Password: "", - InsecureSkipVerify: DefaultInsecureSkipVerify, - Timeout: DefaultTimeOut, - Errors: 0, + name: "No defined values", + config: aapProviderModel{}, + envVars: map[string]string{}, + Host: "", + Username: "", + Password: "", + DefaultOrganization: DefaultOrganization, + InsecureSkipVerify: DefaultInsecureSkipVerify, + Timeout: DefaultTimeOut, + Errors: 0, }, { name: "Using env variables only", @@ -89,55 +91,62 @@ func TestReadValues(t *testing.T) { "AAP_HOST": "https://172.0.0.1:9000", "AAP_USERNAME": "user988", "AAP_PASSWORD": "@pass123#", + "AAP_DEFAULT_ORGANIZATION": "4", "AAP_INSECURE_SKIP_VERIFY": "true", "AAP_TIMEOUT": "30", }, - Host: "https://172.0.0.1:9000", - Username: "user988", - Password: "@pass123#", - InsecureSkipVerify: true, - Timeout: 30, - Errors: 0, + Host: "https://172.0.0.1:9000", + Username: "user988", + Password: "@pass123#", + DefaultOrganization: 4, + InsecureSkipVerify: true, + Timeout: 30, + Errors: 0, }, { name: "Using both configuration and envs value", config: aapProviderModel{ - Host: types.StringValue("https://172.0.0.1:9000"), - Username: types.StringValue("user988"), - Password: types.StringValue("@pass123#"), - InsecureSkipVerify: types.BoolValue(true), - Timeout: types.Int64Value(30), + Host: types.StringValue("https://172.0.0.1:9000"), + Username: types.StringValue("user988"), + Password: types.StringValue("@pass123#"), + DefaultOrganization: types.Int64Value(4), + InsecureSkipVerify: types.BoolValue(true), + Timeout: types.Int64Value(30), }, envVars: map[string]string{ "AAP_HOST": "https://168.3.5.11:8043", "AAP_USERNAME": "ansible", "AAP_PASSWORD": "testing#$%", + "AAP_DEFAULT_ORGANIZATION": "4", "AAP_INSECURE_SKIP_VERIFY": "false", "AAP_TIMEOUT": "3", }, - Host: "https://172.0.0.1:9000", - Username: "user988", - Password: "@pass123#", - InsecureSkipVerify: true, - Timeout: 30, - Errors: 0, + Host: "https://172.0.0.1:9000", + Username: "user988", + Password: "@pass123#", + DefaultOrganization: 4, + InsecureSkipVerify: true, + Timeout: 30, + Errors: 0, }, { name: "Using configuration value", config: aapProviderModel{ - Host: types.StringValue("https://172.0.0.1:9000"), - Username: types.StringValue("user988"), - Password: types.StringValue("@pass123#"), - InsecureSkipVerify: types.BoolValue(true), - Timeout: types.Int64Value(30), + Host: types.StringValue("https://172.0.0.1:9000"), + Username: types.StringValue("user988"), + Password: types.StringValue("@pass123#"), + DefaultOrganization: types.Int64Value(4), + InsecureSkipVerify: types.BoolValue(true), + Timeout: types.Int64Value(30), }, - envVars: map[string]string{}, - Host: "https://172.0.0.1:9000", - Username: "user988", - Password: "@pass123#", - InsecureSkipVerify: true, - Timeout: 30, - Errors: 0, + envVars: map[string]string{}, + Host: "https://172.0.0.1:9000", + Username: "user988", + Password: "@pass123#", + InsecureSkipVerify: true, + DefaultOrganization: 4, + Timeout: 30, + Errors: 0, }, { name: "Bad value for env variable", @@ -151,31 +160,35 @@ func TestReadValues(t *testing.T) { { name: "Using null values in configuration", config: aapProviderModel{ - Host: types.StringNull(), - Username: types.StringNull(), - Password: types.StringNull(), - InsecureSkipVerify: types.BoolNull(), - Timeout: types.Int64Null(), + Host: types.StringNull(), + Username: types.StringNull(), + Password: types.StringNull(), + DefaultOrganization: types.Int64Null(), + InsecureSkipVerify: types.BoolNull(), + Timeout: types.Int64Null(), }, - envVars: map[string]string{}, - Host: "", - Username: "", - Password: "", - InsecureSkipVerify: DefaultInsecureSkipVerify, - Timeout: DefaultTimeOut, - Errors: 0, + envVars: map[string]string{}, + Host: "", + Username: "", + Password: "", + DefaultOrganization: DefaultOrganization, + InsecureSkipVerify: DefaultInsecureSkipVerify, + Timeout: DefaultTimeOut, + Errors: 0, }, } var providerEnvVars = []string{ "AAP_HOST", "AAP_USERNAME", "AAP_PASSWORD", + "AAP_DEFAULT_ORGANIZATION", "AAP_INSECURE_SKIP_VERIFY", "AAP_TIMEOUT", } for _, tc := range testTable { t.Run(tc.name, func(t *testing.T) { var host, username, password string + var defaultOrganization int64 var insecureSkipVerify bool var timeout int64 var resp provider.ConfigureResponse @@ -188,7 +201,7 @@ func TestReadValues(t *testing.T) { } } // ReadValues() - tc.config.ReadValues(&host, &username, &password, &insecureSkipVerify, &timeout, &resp) + tc.config.ReadValues(&host, &username, &password, &defaultOrganization, &insecureSkipVerify, &timeout, &resp) if tc.Errors != resp.Diagnostics.ErrorsCount() { t.Errorf("Errors count expected=(%d) - found=(%d)", tc.Errors, resp.Diagnostics.ErrorsCount()) } else if tc.Errors == 0 { @@ -201,6 +214,9 @@ func TestReadValues(t *testing.T) { if password != tc.Password { t.Errorf("Password values differ expected=(%s) - computed=(%s)", tc.Password, password) } + if defaultOrganization != tc.DefaultOrganization { + t.Errorf("DefaultOrganisation values differ expected=(%d) - computed=(%d)", tc.DefaultOrganization, defaultOrganization) + } if insecureSkipVerify != tc.InsecureSkipVerify { t.Errorf("InsecureSkipVerify values differ expected=(%v) - computed=(%v)", tc.InsecureSkipVerify, insecureSkipVerify) }