From ce4f47db182d5f2f0c287be9fffcf2ad5b3ab2e4 Mon Sep 17 00:00:00 2001 From: Kurt McAlpine Date: Fri, 3 May 2024 17:58:35 +1200 Subject: [PATCH] Implement http_integration --- client/application.go | 41 +++ client/chirpstack.go | 6 + docs/resources/http_integration.md | 30 +++ .../provider/http_integration_resource.go | 251 ++++++++++++++++++ .../http_integration_resource_test.go | 60 +++++ internal/provider/provider.go | 1 + 6 files changed, 389 insertions(+) create mode 100644 docs/resources/http_integration.md create mode 100644 internal/provider/http_integration_resource.go create mode 100644 internal/provider/http_integration_resource_test.go diff --git a/client/application.go b/client/application.go index 1d34840..39bcdd3 100644 --- a/client/application.go +++ b/client/application.go @@ -67,3 +67,44 @@ func (c *chirpstack) DeleteApplication(ctx context.Context, id string) error { } return nil } + +func (c *chirpstack) GetHttpIntegration(ctx context.Context, applicationId string) (*api.HttpIntegration, error) { + req := api.GetHttpIntegrationRequest{ + ApplicationId: applicationId, + } + resp, err := c.applicationServiceClient.GetHttpIntegration(ctx, &req) + if err != nil { + return nil, fmt.Errorf("failed to get http integration for application id %s; err: %s;", applicationId, err) + } + return resp.Integration, nil +} +func (c *chirpstack) CreateHttpIntegration(ctx context.Context, integration *api.HttpIntegration) error { + req := api.CreateHttpIntegrationRequest{ + Integration: integration, + } + _, err := c.applicationServiceClient.CreateHttpIntegration(ctx, &req) + if err != nil { + return fmt.Errorf("failed to create http integration %s; err: %s;", integration, err) + } + return nil +} +func (c *chirpstack) UpdateHttpIntegration(ctx context.Context, integration *api.HttpIntegration) error { + req := api.UpdateHttpIntegrationRequest{ + Integration: integration, + } + _, err := c.applicationServiceClient.UpdateHttpIntegration(ctx, &req) + if err != nil { + return fmt.Errorf("failed to update http integration %s; err: %s;", integration, err) + } + return nil +} +func (c *chirpstack) DeleteHttpIntegration(ctx context.Context, applicationId string) error { + req := api.DeleteHttpIntegrationRequest{ + ApplicationId: applicationId, + } + _, err := c.applicationServiceClient.DeleteHttpIntegration(ctx, &req) + if err != nil { + return fmt.Errorf("failed to delete http integration for application id %s; err: %s;", applicationId, err) + } + return nil +} diff --git a/client/chirpstack.go b/client/chirpstack.go index 5f90d87..36aba0a 100644 --- a/client/chirpstack.go +++ b/client/chirpstack.go @@ -29,6 +29,12 @@ type Chirpstack interface { UpdateApplication(ctx context.Context, application *api.Application) error DeleteApplication(ctx context.Context, id string) error + // integrations + CreateHttpIntegration(ctx context.Context, integration *api.HttpIntegration) error + GetHttpIntegration(ctx context.Context, applicationId string) (*api.HttpIntegration, error) + UpdateHttpIntegration(ctx context.Context, integration *api.HttpIntegration) error + DeleteHttpIntegration(ctx context.Context, applicationId string) error + // messaging Enqueue(ctx context.Context, request *api.EnqueueDeviceQueueItemRequest) (*api.EnqueueDeviceQueueItemResponse, error) MulticastEnqueue(ctx context.Context, request *api.EnqueueMulticastGroupQueueItemRequest) (*api.EnqueueMulticastGroupQueueItemResponse, error) diff --git a/docs/resources/http_integration.md b/docs/resources/http_integration.md new file mode 100644 index 0000000..43f0e78 --- /dev/null +++ b/docs/resources/http_integration.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "chirpstack_http_integration Resource - chirpstack" +subcategory: "" +description: |- + Http Integration resource +--- + +# chirpstack_http_integration (Resource) + +Http Integration resource + + + + +## Schema + +### Required + +- `application_id` (String) Application ID +- `encoding` (String) Http Integration encoding. JSON or PROTOBUF. +- `event_endpoint_url` (String) Http Integration URL + +### Optional + +- `headers` (Map of String) Http Integration headers + +### Read-Only + +- `id` (String) Http Integration identifier diff --git a/internal/provider/http_integration_resource.go b/internal/provider/http_integration_resource.go new file mode 100644 index 0000000..78ca4ae --- /dev/null +++ b/internal/provider/http_integration_resource.go @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/chirpstack/chirpstack/api/go/v4/api" + "github.com/halter-corp/terraform-provider-chirpstack/client" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &HttpIntegrationResource{} +var _ resource.ResourceWithImportState = &HttpIntegrationResource{} + +func NewHttpIntegrationResource() resource.Resource { + return &HttpIntegrationResource{} +} + +// HttpIntegrationResource defines the resource implementation. +type HttpIntegrationResource struct { + chirpstack client.Chirpstack +} + +// HttpIntegrationResourceModel describes the resource data model. +type HttpIntegrationResourceModel struct { + Id types.String `tfsdk:"id"` + ApplicationId types.String `tfsdk:"application_id"` + Headers types.Map `tfsdk:"headers"` + Encoding types.String `tfsdk:"encoding"` + EventEndpointUrl types.String `tfsdk:"event_endpoint_url"` +} + +func (r *HttpIntegrationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_http_integration" +} + +func (r *HttpIntegrationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Http Integration resource", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Http Integration identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "application_id": schema.StringAttribute{ + MarkdownDescription: "Application ID", + Required: true, + }, + "encoding": schema.StringAttribute{ + MarkdownDescription: "Http Integration encoding. JSON or PROTOBUF.", + Required: true, + }, + "event_endpoint_url": schema.StringAttribute{ + MarkdownDescription: "Http Integration URL", + Required: true, + }, + "headers": schema.MapAttribute{ + ElementType: types.StringType, + MarkdownDescription: "Http Integration headers", + Optional: true, + }, + }, + } +} + +func (r *HttpIntegrationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + chirpstack, ok := req.ProviderData.(client.Chirpstack) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.chirpstack = chirpstack +} + +func httpIntegrationFromData(data *HttpIntegrationResourceModel) *api.HttpIntegration { + httpIntegration := &api.HttpIntegration{ + ApplicationId: data.ApplicationId.ValueString(), + Encoding: api.Encoding(api.Encoding_value[data.Encoding.ValueString()]), + EventEndpointUrl: data.EventEndpointUrl.ValueString(), + } + + if !data.Headers.IsNull() { + httpIntegration.Headers = map[string]string{} + for k, v := range data.Headers.Elements() { + httpIntegration.Headers[k] = v.String() + } + } + + return httpIntegration +} + +func httpIntegrationToData(httpIntegration *api.HttpIntegration, data *HttpIntegrationResourceModel) { + data.ApplicationId = types.StringValue(httpIntegration.ApplicationId) + data.Encoding = types.StringValue(httpIntegration.Encoding.String()) + data.EventEndpointUrl = types.StringValue(httpIntegration.EventEndpointUrl) + if len(httpIntegration.Headers) > 0 { + headers := map[string]attr.Value{} + for k, v := range httpIntegration.Headers { + headers[k] = types.StringValue(v) + } + data.Headers = types.MapValueMust(types.StringType, headers) + } +} + +func (r *HttpIntegrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HttpIntegrationResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If applicable, this is a great opportunity to initialize any necessary + // provider client data and make a call using it. + // httpResp, err := r.client.Do(httpReq) + // if err != nil { + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create httpintegration, got error: %s", err)) + // return + // } + httpIntegration := httpIntegrationFromData(&data) + fmt.Printf("create: %+v\n", httpIntegration) + err := r.chirpstack.CreateHttpIntegration(ctx, httpIntegration) + if err != nil { + resp.Diagnostics.AddError("Chirpstack Error", fmt.Sprintf("Unable to create httpintegration, got error: %s", err)) + return + } + + // For the purposes of this httpintegration code, hardcoding a response value to + // save into the Terraform state. + data.Id = types.StringValue(httpIntegration.ApplicationId) + + // Write logs using the tflog package + // Documentation: https://terraform.io/plugin/log + tflog.Trace(ctx, "created a resource") + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *HttpIntegrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data HttpIntegrationResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If applicable, this is a great opportunity to initialize any necessary + // provider client data and make a call using it. + // httpResp, err := r.client.Do(httpReq) + // if err != nil { + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read httpintegration, got error: %s", err)) + // return + // } + httpIntegration, err := r.chirpstack.GetHttpIntegration(ctx, data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Chirpstack Error", fmt.Sprintf("Unable to read httpintegration, got error: %s", err)) + return + } + + httpIntegrationToData(httpIntegration, &data) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *HttpIntegrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data HttpIntegrationResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If applicable, this is a great opportunity to initialize any necessary + // provider client data and make a call using it. + // httpResp, err := r.client.Do(httpReq) + // if err != nil { + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update httpintegration, got error: %s", err)) + // return + // } + httpIntegration := httpIntegrationFromData(&data) + err := r.chirpstack.UpdateHttpIntegration(ctx, httpIntegration) + if err != nil { + resp.Diagnostics.AddError("Chirpstack Error", fmt.Sprintf("Unable to update httpintegration, got error: %s", err)) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *HttpIntegrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data HttpIntegrationResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If applicable, this is a great opportunity to initialize any necessary + // provider client data and make a call using it. + // httpResp, err := r.client.Do(httpReq) + // if err != nil { + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete httpintegration, got error: %s", err)) + // return + // } + err := r.chirpstack.DeleteHttpIntegration(ctx, data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Chirpstack Error", fmt.Sprintf("Unable to delete httpintegration, got error: %s", err)) + return + } +} + +func (r *HttpIntegrationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/http_integration_resource_test.go b/internal/provider/http_integration_resource_test.go new file mode 100644 index 0000000..b2e9872 --- /dev/null +++ b/internal/provider/http_integration_resource_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccHttpIntegrationResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccHttpIntegrationResourceConfig("http://localhost"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("chirpstack_http_integration.test", "id"), + resource.TestCheckResourceAttrSet("chirpstack_http_integration.test", "application_id"), + resource.TestCheckResourceAttr("chirpstack_http_integration.test", "event_endpoint_url", "http://localhost"), + ), + }, + // ImportState testing + { + ResourceName: "chirpstack_http_integration.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update and Read testing + { + Config: testAccHttpIntegrationResourceConfig("two"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("chirpstack_http_integration.test", "event_endpoint_url", "two"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccHttpIntegrationResourceConfig(endpoint string) string { + return fmt.Sprintf(` +resource "chirpstack_tenant" "test" { + name = "test_tenant" +} +resource "chirpstack_application" "test" { + tenant_id = chirpstack_tenant.test.id + name = "test_app" +} +resource "chirpstack_http_integration" "test" { + application_id = chirpstack_application.test.id + encoding = "JSON" + event_endpoint_url = %[1]q +} +`, endpoint) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 82769de..4357e88 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -106,6 +106,7 @@ func (p *ChirpstackProvider) Resources(ctx context.Context) []func() resource.Re NewTenantResource, NewApplicationResource, NewDeviceProfileResource, + NewHttpIntegrationResource, } }