From e2ad037e4025fdcd376359f857c74bbca7178da3 Mon Sep 17 00:00:00 2001 From: catsby Date: Wed, 13 Mar 2024 12:58:04 -0500 Subject: [PATCH 01/17] re-stub the waypoint application resource --- internal/clients/waypoint.go | 38 ++ internal/provider/provider.go | 1 + .../waypoint/resource_waypoint_application.go | 532 ++++++++++++++++++ .../resource_waypoint_application_test.go | 136 +++++ 4 files changed, 707 insertions(+) create mode 100644 internal/provider/waypoint/resource_waypoint_application.go create mode 100644 internal/provider/waypoint/resource_waypoint_application_test.go diff --git a/internal/clients/waypoint.go b/internal/clients/waypoint.go index cb830b86b..b63af83c8 100644 --- a/internal/clients/waypoint.go +++ b/internal/clients/waypoint.go @@ -101,3 +101,41 @@ func GetAddOnDefinitionByID(ctx context.Context, client *Client, loc *sharedmode } return getResp.GetPayload().AddOnDefinition, nil } + +// GetApplicationByName will retrieve an application by name +func GetApplicationByName(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, appName string) (*waypoint_models.HashicorpCloudWaypointApplication, error) { + ns, err := getNamespaceByLocation(ctx, client, loc) + if err != nil { + return nil, err + } + + params := &waypoint_service.WaypointServiceGetApplication2Params{ + ApplicationName: appName, + NamespaceID: ns.ID, + } + + getResp, err := client.Waypoint.WaypointServiceGetApplication2(params, nil) + if err != nil { + return nil, err + } + return getResp.GetPayload().Application, nil +} + +// GetApplicationByID will retrieve an application by ID +func GetApplicationByID(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, appID string) (*waypoint_models.HashicorpCloudWaypointApplication, error) { + ns, err := getNamespaceByLocation(ctx, client, loc) + if err != nil { + return nil, err + } + + params := &waypoint_service.WaypointServiceGetApplicationParams{ + ApplicationID: appID, + NamespaceID: ns.ID, + } + + getResp, err := client.Waypoint.WaypointServiceGetApplication(params, nil) + if err != nil { + return nil, err + } + return getResp.GetPayload().Application, nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5486c4d85..8ed2af135 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -151,6 +151,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res // Webhook webhook.NewNotificationsWebhookResource, // Waypoint + waypoint.NewApplicationResource, waypoint.NewApplicationTemplateResource, waypoint.NewAddOnDefinitionResource, waypoint.NewTfcConfigResource, diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go new file mode 100644 index 000000000..4b04a0244 --- /dev/null +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -0,0 +1,532 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package waypoint + +import ( + "context" + "fmt" + + "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" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ApplicationResource{} +var _ resource.ResourceWithImportState = &ApplicationResource{} + +func NewApplicationResource() resource.Resource { + return &ApplicationResource{} +} + +// ApplicationResource defines the resource implementation. +type ApplicationResource struct { + client *clients.Client +} + +// ApplicationResourceModel describes the resource data model. +type ApplicationResourceModel struct { + ID types.String `tfsdk:"id"` + // Questionable if this should exist or just use external_id -> ID + ExternalID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ProjectID types.String `tfsdk:"project_id"` + OrgID types.String `tfsdk:"organization_id"` + ReadmeMarkdown types.String `tfsdk:"readme_markdown"` + ApplicationTemplateID types.String `tfsdk:"application_template_id"` + ApplicationTemplateName types.String `tfsdk:"application_template_name"` + NamespaceID types.String `tfsdk:"namespace_id"` + + // deferred for now + // Tags types.List `tfsdk:"tags"` + // CreatedAt types.String `tfsdk:"created_at"` + // UpdatedAt types.String `tfsdk:"updated_at"` + + // deferred and probably a list or objects + // ActionCfgs types.List `tfsdk:"action_cfgs"` +} + +func (r *ApplicationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_waypoint_application" +} + +func (r *ApplicationResource) 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: "Waypoint Application resource", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of the Application.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the Application.", + Required: true, + }, + "organization_id": schema.StringAttribute{ + Description: "The ID of the HCP organization where the Waypoint Application is located.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the HCP project where the Waypoint Application is located.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "application_template_id": schema.StringAttribute{ + Required: true, + Description: "ID of the Application Template this Application is based on.", + }, + // TODO(clint): not sure why both are in the model + "application_template_name": schema.StringAttribute{ + Computed: true, + Description: "Name of the Application Template this Application is based on.", + }, + "readme_markdown": schema.StringAttribute{ + Computed: true, + Optional: true, + Description: "Instructions for using the Application (markdown format supported)", + }, + "namespace_id": schema.StringAttribute{ + Computed: true, + Description: "Internal Namespace ID.", + }, + }, + } +} + +func (r *ApplicationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*clients.Client) + 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.client = client +} + +func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan *ApplicationResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + // projectID := r.client.Config.ProjectID + // if !plan.ProjectID.IsUnknown() { + // projectID = plan.ProjectID.ValueString() + // } + + // orgID := r.client.Config.OrganizationID + // loc := &sharedmodels.HashicorpCloudLocationLocation{ + // OrganizationID: orgID, + // ProjectID: projectID, + // } + + // client := r.client + // ns, err := getNamespaceByLocation(ctx, client, loc) + // if err != nil { + // resp.Diagnostics.AddError( + // "error getting namespace by location", + // err.Error(), + // ) + // return + // } + + // strLabels := []string{} + // diags := plan.Labels.ElementsAs(ctx, &strLabels, false) + // if diags.HasError() { + // return + // } + + // readmeBytes, err := base64.StdEncoding.DecodeString(plan.ReadmeMarkdown.ValueString()) + // if err != nil { + // resp.Diagnostics.AddError( + // "error decoding the base64 file contents", + // err.Error(), + // ) + // } + + // modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceCreateApplicationTemplateBody{ + // ApplicationTemplate: &waypoint_models.HashicorpCloudWaypointApplicationTemplate{ + // Name: plan.Name.ValueString(), + // Summary: plan.Summary.ValueString(), + // Labels: strLabels, + // Description: plan.Description.ValueString(), + // ReadmeMarkdownTemplate: readmeBytes, + // TerraformNocodeModule: &waypoint_models.HashicorpCloudWaypointTerraformNocodeModule{ + // Source: plan.TerraformNoCodeModule.Source.ValueString(), + // Version: plan.TerraformNoCodeModule.Version.ValueString(), + // }, + // TerraformCloudWorkspaceDetails: &waypoint_models.HashicorpCloudWaypointTerraformCloudWorkspaceDetails{ + // Name: plan.TerraformCloudWorkspace.Name.ValueString(), + // ProjectID: plan.TerraformCloudWorkspace.TerraformProjectID.ValueString(), + // }, + // }, + // } + + // params := &waypoint_service.WaypointServiceCreateApplicationTemplateParams{ + // NamespaceID: ns.ID, + // Body: modelBody, + // } + // app, err := r.client.Waypoint.WaypointServiceCreateApplicationTemplate(params, nil) + // if err != nil { + // resp.Diagnostics.AddError("Error creating application template", err.Error()) + // return + // } + + // var appTemplate *waypoint_models.HashicorpCloudWaypointApplicationTemplate + // if app.Payload != nil { + // appTemplate = app.Payload.ApplicationTemplate + // } + // if appTemplate == nil { + // resp.Diagnostics.AddError("unknown error creating application template", "empty application template found") + // return + // } + + // plan.ID = types.StringValue(appTemplate.ID) + // plan.ProjectID = types.StringValue(projectID) + // plan.Name = types.StringValue(appTemplate.Name) + // plan.OrgID = types.StringValue(orgID) + // plan.Summary = types.StringValue(appTemplate.Summary) + + // if appTemplate.TerraformCloudWorkspaceDetails != nil { + // tfcWorkspace := &tfcWorkspace{ + // Name: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.Name), + // TerraformProjectID: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.ProjectID), + // } + // plan.TerraformCloudWorkspace = tfcWorkspace + // } + + // if appTemplate.TerraformNocodeModule != nil { + // tfcNoCode := &tfcNoCodeModule{ + // Source: types.StringValue(appTemplate.TerraformNocodeModule.Source), + // Version: types.StringValue(appTemplate.TerraformNocodeModule.Version), + // } + // plan.TerraformNoCodeModule = tfcNoCode + // } + + // labels, diags := types.ListValueFrom(ctx, types.StringType, appTemplate.Labels) + // resp.Diagnostics.Append(diags...) + // if resp.Diagnostics.HasError() { + // return + // } + // if len(labels.Elements()) == 0 { + // labels = types.ListNull(types.StringType) + // } + + // plan.Labels = labels + + // // set plan.description if it's not null or appTemplate.description is not + // // empty + // plan.Description = types.StringValue(appTemplate.Description) + // if appTemplate.Description == "" { + // plan.Description = types.StringNull() + // } + // // set plan.readme if it's not null or appTemplate.readme is not + // // empty + // plan.ReadmeMarkdown = types.StringValue(appTemplate.ReadmeMarkdownTemplate.String()) + // if appTemplate.ReadmeMarkdownTemplate.String() == "" { + // plan.ReadmeMarkdown = types.StringNull() + // } + + // Write logs using the tflog package + // Documentation: https://terraform.io/plugin/log + tflog.Trace(ctx, "created application template resource") + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ApplicationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *ApplicationResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // projectID := r.client.Config.ProjectID + // if !data.ProjectID.IsUnknown() { + // projectID = data.ProjectID.ValueString() + // } + + // orgID := r.client.Config.OrganizationID + // loc := &sharedmodels.HashicorpCloudLocationLocation{ + // OrganizationID: orgID, + // ProjectID: projectID, + // } + + // client := r.client + + // appTemplate, err := clients.GetApplicationTemplateByID(ctx, client, loc, data.ID.ValueString()) + // if err != nil { + // if clients.IsResponseCodeNotFound(err) { + // tflog.Info(ctx, "TFC Config not found for organization, removing from state.") + // resp.State.RemoveResource(ctx) + // return + // } + // resp.Diagnostics.AddError("Error reading TFC Config", err.Error()) + // return + // } + + // data.ID = types.StringValue(appTemplate.ID) + // data.Name = types.StringValue(appTemplate.Name) + // data.OrgID = types.StringValue(client.Config.OrganizationID) + // data.ProjectID = types.StringValue(client.Config.ProjectID) + // data.Summary = types.StringValue(appTemplate.Summary) + + // if appTemplate.TerraformCloudWorkspaceDetails != nil { + // tfcWorkspace := &tfcWorkspace{ + // Name: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.Name), + // TerraformProjectID: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.ProjectID), + // } + // data.TerraformCloudWorkspace = tfcWorkspace + // } + + // if appTemplate.TerraformNocodeModule != nil { + // tfcNoCode := &tfcNoCodeModule{ + // Source: types.StringValue(appTemplate.TerraformNocodeModule.Source), + // Version: types.StringValue(appTemplate.TerraformNocodeModule.Version), + // } + // data.TerraformNoCodeModule = tfcNoCode + // } + + // labels, diags := types.ListValueFrom(ctx, types.StringType, appTemplate.Labels) + // resp.Diagnostics.Append(diags...) + // if resp.Diagnostics.HasError() { + // return + // } + // if len(labels.Elements()) == 0 { + // labels = types.ListNull(types.StringType) + // } + // data.Labels = labels + + // // set data.description if it's not null or appTemplate.description is not + // // empty + // data.Description = types.StringValue(appTemplate.Description) + // if appTemplate.Description == "" { + // data.Description = types.StringNull() + // } + // // set data.readme if it's not null or appTemplate.readme is not + // // empty + // data.ReadmeMarkdown = types.StringValue(appTemplate.ReadmeMarkdownTemplate.String()) + // if appTemplate.ReadmeMarkdownTemplate.String() == "" { + // data.ReadmeMarkdown = types.StringNull() + // } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ApplicationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan *ApplicationResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + // projectID := r.client.Config.ProjectID + // if !plan.ProjectID.IsUnknown() { + // projectID = plan.ProjectID.ValueString() + // } + + // orgID := r.client.Config.OrganizationID + // loc := &sharedmodels.HashicorpCloudLocationLocation{ + // OrganizationID: orgID, + // ProjectID: projectID, + // } + + // client := r.client + // ns, err := getNamespaceByLocation(ctx, client, loc) + // if err != nil { + // resp.Diagnostics.AddError( + // "error getting namespace by location", + // err.Error(), + // ) + // return + // } + + // strLabels := []string{} + // diags := plan.Labels.ElementsAs(ctx, &strLabels, false) + // if diags.HasError() { + // return + // } + + // readmeBytes, err := base64.StdEncoding.DecodeString(plan.ReadmeMarkdown.ValueString()) + // if err != nil { + // resp.Diagnostics.AddError( + // "error decoding the base64 file contents", + // err.Error(), + // ) + // return + // } + + // modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceUpdateApplicationTemplateBody{ + // ApplicationTemplate: &waypoint_models.HashicorpCloudWaypointApplicationTemplate{ + // Name: plan.Name.ValueString(), + // Summary: plan.Summary.ValueString(), + // Labels: strLabels, + // Description: plan.Description.ValueString(), + // ReadmeMarkdownTemplate: readmeBytes, + // TerraformNocodeModule: &waypoint_models.HashicorpCloudWaypointTerraformNocodeModule{ + // Source: plan.TerraformNoCodeModule.Source.ValueString(), + // Version: plan.TerraformNoCodeModule.Version.ValueString(), + // }, + // TerraformCloudWorkspaceDetails: &waypoint_models.HashicorpCloudWaypointTerraformCloudWorkspaceDetails{ + // Name: plan.TerraformCloudWorkspace.Name.ValueString(), + // ProjectID: plan.TerraformCloudWorkspace.TerraformProjectID.ValueString(), + // }, + // }, + // } + + // params := &waypoint_service.WaypointServiceUpdateApplicationTemplateParams{ + // NamespaceID: ns.ID, + // Body: modelBody, + // ExistingApplicationTemplateID: plan.ID.ValueString(), + // } + // app, err := r.client.Waypoint.WaypointServiceUpdateApplicationTemplate(params, nil) + // if err != nil { + // resp.Diagnostics.AddError("Error updating project", err.Error()) + // return + // } + + // var appTemplate *waypoint_models.HashicorpCloudWaypointApplicationTemplate + // if app.Payload != nil { + // appTemplate = app.Payload.ApplicationTemplate + // } + // if appTemplate == nil { + // resp.Diagnostics.AddError("unknown error updating application template", "empty application template found") + // return + // } + + // plan.ID = types.StringValue(appTemplate.ID) + // plan.ProjectID = types.StringValue(projectID) + // plan.Name = types.StringValue(appTemplate.Name) + // plan.OrgID = types.StringValue(orgID) + // plan.Summary = types.StringValue(appTemplate.Summary) + + // labels, diags := types.ListValueFrom(ctx, types.StringType, appTemplate.Labels) + // resp.Diagnostics.Append(diags...) + // if resp.Diagnostics.HasError() { + // return + // } + // plan.Labels = labels + + // if appTemplate.TerraformCloudWorkspaceDetails != nil { + // tfcWorkspace := &tfcWorkspace{ + // Name: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.Name), + // TerraformProjectID: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.ProjectID), + // } + // plan.TerraformCloudWorkspace = tfcWorkspace + // } + + // if appTemplate.TerraformNocodeModule != nil { + // tfcNoCode := &tfcNoCodeModule{ + // Source: types.StringValue(appTemplate.TerraformNocodeModule.Source), + // Version: types.StringValue(appTemplate.TerraformNocodeModule.Version), + // } + // plan.TerraformNoCodeModule = tfcNoCode + // } + + // plan.Description = types.StringValue(appTemplate.Description) + // if appTemplate.Description == "" { + // plan.Description = types.StringNull() + // } + // plan.ReadmeMarkdown = types.StringValue(appTemplate.ReadmeMarkdownTemplate.String()) + // if appTemplate.ReadmeMarkdownTemplate.String() == "" { + // plan.ReadmeMarkdown = types.StringNull() + // } + + // Write logs using the tflog package + // Documentation: https://terraform.io/plugin/log + tflog.Trace(ctx, "updated application template resource") + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ApplicationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *ApplicationResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // projectID := r.client.Config.ProjectID + // if !data.ProjectID.IsUnknown() { + // projectID = data.ProjectID.ValueString() + // } + + // loc := &sharedmodels.HashicorpCloudLocationLocation{ + // OrganizationID: r.client.Config.OrganizationID, + // ProjectID: projectID, + // } + + // client := r.client + // ns, err := getNamespaceByLocation(ctx, client, loc) + // if err != nil { + // resp.Diagnostics.AddError( + // "Error Deleting TFC Config", + // err.Error(), + // ) + // return + // } + + // params := &waypoint_service.WaypointServiceDeleteApplicationTemplateParams{ + // NamespaceID: ns.ID, + // ApplicationTemplateID: data.ID.ValueString(), + // } + + // _, err = r.client.Waypoint.WaypointServiceDeleteApplicationTemplate(params, nil) + + // if err != nil { + // if clients.IsResponseCodeNotFound(err) { + // tflog.Info(ctx, "Application Template not found for organization during delete call, ignoring") + // return + // } + // resp.Diagnostics.AddError( + // "Error Deleting Application Template", + // err.Error(), + // ) + // return + // } +} + +func (r *ApplicationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/waypoint/resource_waypoint_application_test.go b/internal/provider/waypoint/resource_waypoint_application_test.go new file mode 100644 index 000000000..a1e3fefc4 --- /dev/null +++ b/internal/provider/waypoint/resource_waypoint_application_test.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package waypoint_test + +import ( + "context" + "fmt" + "testing" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/waypoint" +) + +func TestAccWaypoint_Application_basic(t *testing.T) { + var applicationModel waypoint.ApplicationResourceModel + resourceName := "hcp_waypoint_application.test" + name := generateRandomName() + updatedName := generateRandomName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: testAccCheckWaypointApplicationDestroy(t, &applicationModel), + Steps: []resource.TestStep{ + { + Config: testApplicationConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckWaypointApplicationExists(t, resourceName, &applicationModel), + testAccCheckWaypointApplicationName(t, &applicationModel, name), + resource.TestCheckResourceAttr(resourceName, "name", name), + ), + }, + { + Config: testApplicationConfig(updatedName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWaypointApplicationExists(t, resourceName, &applicationModel), + testAccCheckWaypointApplicationName(t, &applicationModel, updatedName), + resource.TestCheckResourceAttr(resourceName, "name", updatedName), + ), + }, + }, + }) +} + +// simple attribute check on the application receved from the API +func testAccCheckWaypointApplicationName(_ *testing.T, applicationModel *waypoint.ApplicationResourceModel, nameValue string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if applicationModel.Name.ValueString() != nameValue { + return fmt.Errorf("expected application name to be %s, but got %s", nameValue, applicationModel.Name.ValueString()) + } + return nil + } +} + +func testAccCheckWaypointApplicationExists(t *testing.T, resourceName string, applicationModel *waypoint.ApplicationResourceModel) resource.TestCheckFunc { + return func(s *terraform.State) error { + // find the corresponding state object + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + client := acctest.HCPClients(t) + // Get the project ID and ID from state + projectID := rs.Primary.Attributes["project_id"] + appID := rs.Primary.Attributes["id"] + orgID := client.Config.OrganizationID + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: orgID, + ProjectID: projectID, + } + + // Fetch the application + application, err := clients.GetApplicationByID(context.Background(), client, loc, appID) + if err != nil { + return err + } + + // at this time we're only verifing existence and not checking all the + // values, so only set name,id, and project id for now + if applicationModel != nil { + applicationModel.Name = types.StringValue(application.Name) + applicationModel.ID = types.StringValue(application.ID) + applicationModel.ExternalID = types.StringValue(application.ID) + applicationModel.ProjectID = types.StringValue(projectID) + } + + return nil + } +} + +func testAccCheckWaypointApplicationDestroy(t *testing.T, applicationModel *waypoint.ApplicationResourceModel) resource.TestCheckFunc { + return func(_ *terraform.State) error { + client := acctest.HCPClients(t) + id := applicationModel.ID.ValueString() + projectID := applicationModel.ProjectID.ValueString() + orgID := client.Config.OrganizationID + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: orgID, + ProjectID: projectID, + } + + application, err := clients.GetApplicationByID(context.Background(), client, loc, id) + if err != nil { + // expected + if clients.IsResponseCodeNotFound(err) { + return nil + } + return err + } + + // fall through, we expect a not found above but if we get this far then + // the test should fail + if application != nil { + return fmt.Errorf("expected application to be destroyed, but it still exists") + } + + return fmt.Errorf("both application and error were nil in destroy check, this should not happen") + } +} + +func testApplicationConfig(name string) string { + return fmt.Sprintf(` +resource "hcp_waypoint_application" "test" { + name = %q + summary = "some summary for fun" +}`, name) +} From 10bd22a259f69f085fd452fa83e34268f21a8407 Mon Sep 17 00:00:00 2001 From: catsby Date: Wed, 13 Mar 2024 14:12:48 -0500 Subject: [PATCH 02/17] basic failing test, remove external ID --- .../provider/waypoint/resource_waypoint_application.go | 4 +--- .../waypoint/resource_waypoint_application_test.go | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index 4b04a0244..a8d19d32f 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -32,9 +32,7 @@ type ApplicationResource struct { // ApplicationResourceModel describes the resource data model. type ApplicationResourceModel struct { - ID types.String `tfsdk:"id"` - // Questionable if this should exist or just use external_id -> ID - ExternalID types.String `tfsdk:"id"` + ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` ProjectID types.String `tfsdk:"project_id"` OrgID types.String `tfsdk:"organization_id"` diff --git a/internal/provider/waypoint/resource_waypoint_application_test.go b/internal/provider/waypoint/resource_waypoint_application_test.go index a1e3fefc4..1b719bf78 100644 --- a/internal/provider/waypoint/resource_waypoint_application_test.go +++ b/internal/provider/waypoint/resource_waypoint_application_test.go @@ -88,7 +88,6 @@ func testAccCheckWaypointApplicationExists(t *testing.T, resourceName string, ap if applicationModel != nil { applicationModel.Name = types.StringValue(application.Name) applicationModel.ID = types.StringValue(application.ID) - applicationModel.ExternalID = types.StringValue(application.ID) applicationModel.ProjectID = types.StringValue(projectID) } @@ -129,8 +128,10 @@ func testAccCheckWaypointApplicationDestroy(t *testing.T, applicationModel *wayp func testApplicationConfig(name string) string { return fmt.Sprintf(` +%s + resource "hcp_waypoint_application" "test" { name = %q - summary = "some summary for fun" -}`, name) + application_template_id = hcp_waypoint_application_template.test.id +}`, testAppTemplateConfig(name), name) } From 6057dc3220718159f9588d9b374be916ba553db1 Mon Sep 17 00:00:00 2001 From: catsby Date: Fri, 15 Mar 2024 14:28:30 -0500 Subject: [PATCH 03/17] basic implementation --- .../waypoint/resource_waypoint_application.go | 277 ++++++------------ 1 file changed, 96 insertions(+), 181 deletions(-) diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index a8d19d32f..843aeb3e8 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -7,6 +7,9 @@ import ( "context" "fmt" + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/client/waypoint_service" + waypoint_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/models" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -93,6 +96,7 @@ func (r *ApplicationResource) Schema(ctx context.Context, req resource.SchemaReq // TODO(clint): not sure why both are in the model "application_template_name": schema.StringAttribute{ Computed: true, + Optional: true, Description: "Name of the Application Template this Application is based on.", }, "readme_markdown": schema.StringAttribute{ @@ -137,127 +141,70 @@ func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateReq return } - // projectID := r.client.Config.ProjectID - // if !plan.ProjectID.IsUnknown() { - // projectID = plan.ProjectID.ValueString() - // } - - // orgID := r.client.Config.OrganizationID - // loc := &sharedmodels.HashicorpCloudLocationLocation{ - // OrganizationID: orgID, - // ProjectID: projectID, - // } - - // client := r.client - // ns, err := getNamespaceByLocation(ctx, client, loc) - // if err != nil { - // resp.Diagnostics.AddError( - // "error getting namespace by location", - // err.Error(), - // ) - // return - // } - - // strLabels := []string{} - // diags := plan.Labels.ElementsAs(ctx, &strLabels, false) - // if diags.HasError() { - // return - // } - - // readmeBytes, err := base64.StdEncoding.DecodeString(plan.ReadmeMarkdown.ValueString()) - // if err != nil { - // resp.Diagnostics.AddError( - // "error decoding the base64 file contents", - // err.Error(), - // ) - // } - - // modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceCreateApplicationTemplateBody{ - // ApplicationTemplate: &waypoint_models.HashicorpCloudWaypointApplicationTemplate{ - // Name: plan.Name.ValueString(), - // Summary: plan.Summary.ValueString(), - // Labels: strLabels, - // Description: plan.Description.ValueString(), - // ReadmeMarkdownTemplate: readmeBytes, - // TerraformNocodeModule: &waypoint_models.HashicorpCloudWaypointTerraformNocodeModule{ - // Source: plan.TerraformNoCodeModule.Source.ValueString(), - // Version: plan.TerraformNoCodeModule.Version.ValueString(), - // }, - // TerraformCloudWorkspaceDetails: &waypoint_models.HashicorpCloudWaypointTerraformCloudWorkspaceDetails{ - // Name: plan.TerraformCloudWorkspace.Name.ValueString(), - // ProjectID: plan.TerraformCloudWorkspace.TerraformProjectID.ValueString(), - // }, - // }, - // } - - // params := &waypoint_service.WaypointServiceCreateApplicationTemplateParams{ - // NamespaceID: ns.ID, - // Body: modelBody, - // } - // app, err := r.client.Waypoint.WaypointServiceCreateApplicationTemplate(params, nil) - // if err != nil { - // resp.Diagnostics.AddError("Error creating application template", err.Error()) - // return - // } - - // var appTemplate *waypoint_models.HashicorpCloudWaypointApplicationTemplate - // if app.Payload != nil { - // appTemplate = app.Payload.ApplicationTemplate - // } - // if appTemplate == nil { - // resp.Diagnostics.AddError("unknown error creating application template", "empty application template found") - // return - // } - - // plan.ID = types.StringValue(appTemplate.ID) - // plan.ProjectID = types.StringValue(projectID) - // plan.Name = types.StringValue(appTemplate.Name) - // plan.OrgID = types.StringValue(orgID) - // plan.Summary = types.StringValue(appTemplate.Summary) - - // if appTemplate.TerraformCloudWorkspaceDetails != nil { - // tfcWorkspace := &tfcWorkspace{ - // Name: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.Name), - // TerraformProjectID: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.ProjectID), - // } - // plan.TerraformCloudWorkspace = tfcWorkspace - // } + projectID := r.client.Config.ProjectID + if !plan.ProjectID.IsUnknown() { + projectID = plan.ProjectID.ValueString() + } - // if appTemplate.TerraformNocodeModule != nil { - // tfcNoCode := &tfcNoCodeModule{ - // Source: types.StringValue(appTemplate.TerraformNocodeModule.Source), - // Version: types.StringValue(appTemplate.TerraformNocodeModule.Version), - // } - // plan.TerraformNoCodeModule = tfcNoCode - // } - - // labels, diags := types.ListValueFrom(ctx, types.StringType, appTemplate.Labels) - // resp.Diagnostics.Append(diags...) - // if resp.Diagnostics.HasError() { - // return - // } - // if len(labels.Elements()) == 0 { - // labels = types.ListNull(types.StringType) - // } - - // plan.Labels = labels - - // // set plan.description if it's not null or appTemplate.description is not - // // empty - // plan.Description = types.StringValue(appTemplate.Description) - // if appTemplate.Description == "" { - // plan.Description = types.StringNull() - // } - // // set plan.readme if it's not null or appTemplate.readme is not - // // empty - // plan.ReadmeMarkdown = types.StringValue(appTemplate.ReadmeMarkdownTemplate.String()) - // if appTemplate.ReadmeMarkdownTemplate.String() == "" { - // plan.ReadmeMarkdown = types.StringNull() - // } + orgID := r.client.Config.OrganizationID + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: orgID, + ProjectID: projectID, + } + + client := r.client + ns, err := getNamespaceByLocation(ctx, client, loc) + if err != nil { + resp.Diagnostics.AddError( + "error getting namespace by location", + err.Error(), + ) + return + } + + modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceCreateApplicationFromTemplateBody{ + Name: plan.Name.ValueString(), + ApplicationTemplate: &waypoint_models.HashicorpCloudWaypointRefApplicationTemplate{ + // ID: plan.ApplicationTemplateID.ValueString(), + Name: plan.ApplicationTemplateName.ValueString(), + }, + } + + params := &waypoint_service.WaypointServiceCreateApplicationFromTemplateParams{ + NamespaceID: ns.ID, + Body: modelBody, + } + app, err := r.client.Waypoint.WaypointServiceCreateApplicationFromTemplate(params, nil) + if err != nil { + resp.Diagnostics.AddError("Error creating application from template", err.Error()) + return + } + + var application *waypoint_models.HashicorpCloudWaypointApplication + if app.Payload != nil { + application = app.Payload.Application + } + if application == nil { + resp.Diagnostics.AddError("unknown error creating application from template", "empty application template found") + return + } + + plan.ID = types.StringValue(application.ID) + plan.ProjectID = types.StringValue(projectID) + plan.Name = types.StringValue(application.Name) + plan.OrgID = types.StringValue(orgID) + plan.NamespaceID = types.StringValue(ns.ID) + + // set plan.readme if it's not null or appTemplate.readme is not + // empty + plan.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) + if application.ReadmeMarkdown.String() == "" { + plan.ReadmeMarkdown = types.StringNull() + } // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log - tflog.Trace(ctx, "created application template resource") + tflog.Trace(ctx, "created application from template resource") // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -273,74 +220,42 @@ func (r *ApplicationResource) Read(ctx context.Context, req resource.ReadRequest return } - // projectID := r.client.Config.ProjectID - // if !data.ProjectID.IsUnknown() { - // projectID = data.ProjectID.ValueString() - // } - - // orgID := r.client.Config.OrganizationID - // loc := &sharedmodels.HashicorpCloudLocationLocation{ - // OrganizationID: orgID, - // ProjectID: projectID, - // } - - // client := r.client - - // appTemplate, err := clients.GetApplicationTemplateByID(ctx, client, loc, data.ID.ValueString()) - // if err != nil { - // if clients.IsResponseCodeNotFound(err) { - // tflog.Info(ctx, "TFC Config not found for organization, removing from state.") - // resp.State.RemoveResource(ctx) - // return - // } - // resp.Diagnostics.AddError("Error reading TFC Config", err.Error()) - // return - // } + projectID := r.client.Config.ProjectID + if !data.ProjectID.IsUnknown() { + projectID = data.ProjectID.ValueString() + } - // data.ID = types.StringValue(appTemplate.ID) - // data.Name = types.StringValue(appTemplate.Name) - // data.OrgID = types.StringValue(client.Config.OrganizationID) - // data.ProjectID = types.StringValue(client.Config.ProjectID) - // data.Summary = types.StringValue(appTemplate.Summary) + orgID := r.client.Config.OrganizationID + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: orgID, + ProjectID: projectID, + } - // if appTemplate.TerraformCloudWorkspaceDetails != nil { - // tfcWorkspace := &tfcWorkspace{ - // Name: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.Name), - // TerraformProjectID: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.ProjectID), - // } - // data.TerraformCloudWorkspace = tfcWorkspace - // } + client := r.client - // if appTemplate.TerraformNocodeModule != nil { - // tfcNoCode := &tfcNoCodeModule{ - // Source: types.StringValue(appTemplate.TerraformNocodeModule.Source), - // Version: types.StringValue(appTemplate.TerraformNocodeModule.Version), - // } - // data.TerraformNoCodeModule = tfcNoCode - // } - - // labels, diags := types.ListValueFrom(ctx, types.StringType, appTemplate.Labels) - // resp.Diagnostics.Append(diags...) - // if resp.Diagnostics.HasError() { - // return - // } - // if len(labels.Elements()) == 0 { - // labels = types.ListNull(types.StringType) - // } - // data.Labels = labels + application, err := clients.GetApplicationByID(ctx, client, loc, data.ID.ValueString()) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + tflog.Info(ctx, "TFC Config not found for organization, removing from state.") + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error reading TFC Config", err.Error()) + return + } - // // set data.description if it's not null or appTemplate.description is not - // // empty - // data.Description = types.StringValue(appTemplate.Description) - // if appTemplate.Description == "" { - // data.Description = types.StringNull() - // } - // // set data.readme if it's not null or appTemplate.readme is not - // // empty - // data.ReadmeMarkdown = types.StringValue(appTemplate.ReadmeMarkdownTemplate.String()) - // if appTemplate.ReadmeMarkdownTemplate.String() == "" { - // data.ReadmeMarkdown = types.StringNull() - // } + data.ID = types.StringValue(application.ID) + data.ProjectID = types.StringValue(projectID) + data.Name = types.StringValue(application.Name) + data.OrgID = types.StringValue(orgID) + // data.NamespaceID = types.StringValue(ns.ID) + + // set plan.readme if it's not null or appTemplate.readme is not + // empty + data.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) + if application.ReadmeMarkdown.String() == "" { + data.ReadmeMarkdown = types.StringNull() + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } From 63ac1c9eb4430182c57703913e68b2fd583b2ef6 Mon Sep 17 00:00:00 2001 From: catsby Date: Fri, 15 Mar 2024 14:31:54 -0500 Subject: [PATCH 04/17] basic docs and some typo cleanup --- docs/resources/waypoint_application.md | 33 +++++++++++++++++++ .../waypoint_application_template.md | 2 +- .../waypoint/resource_waypoint_application.go | 5 +-- .../resource_waypoint_application_template.go | 6 ++-- 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 docs/resources/waypoint_application.md diff --git a/docs/resources/waypoint_application.md b/docs/resources/waypoint_application.md new file mode 100644 index 000000000..f386c07ea --- /dev/null +++ b/docs/resources/waypoint_application.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_waypoint_application Resource - terraform-provider-hcp" +subcategory: "" +description: |- + Waypoint Application resource +--- + +# hcp_waypoint_application (Resource) + +Waypoint Application resource + + + + +## Schema + +### Required + +- `application_template_id` (String) ID of the Application Template this Application is based on. +- `name` (String) The name of the Application. + +### Optional + +- `application_template_name` (String) Name of the Application Template this Application is based on. +- `project_id` (String) The ID of the HCP project where the Waypoint Application is located. +- `readme_markdown` (String) Instructions for using the Application (markdown format supported) + +### Read-Only + +- `id` (String) The ID of the Application. +- `namespace_id` (String) Internal Namespace ID. +- `organization_id` (String) The ID of the HCP organization where the Waypoint Application is located. diff --git a/docs/resources/waypoint_application_template.md b/docs/resources/waypoint_application_template.md index c1d897376..d02e3e59b 100644 --- a/docs/resources/waypoint_application_template.md +++ b/docs/resources/waypoint_application_template.md @@ -40,7 +40,7 @@ Waypoint Application Template resource Required: - `name` (String) Name of the Terraform Cloud Workspace -- `terraform_project_id` (String) Tetraform Cloud Project ID +- `terraform_project_id` (String) Terraform Cloud Project ID diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index 843aeb3e8..7384ea514 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -185,7 +185,7 @@ func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateReq application = app.Payload.Application } if application == nil { - resp.Diagnostics.AddError("unknown error creating application from template", "empty application template found") + resp.Diagnostics.AddError("unknown error creating application from template", "empty application template returned") return } @@ -340,7 +340,8 @@ func (r *ApplicationResource) Update(ctx context.Context, req resource.UpdateReq // appTemplate = app.Payload.ApplicationTemplate // } // if appTemplate == nil { - // resp.Diagnostics.AddError("unknown error updating application template", "empty application template found") + // resp.Diagnostics.AddError("unknown error updating application + // template", "empty application template returned") // return // } diff --git a/internal/provider/waypoint/resource_waypoint_application_template.go b/internal/provider/waypoint/resource_waypoint_application_template.go index 75cb9f21f..3405f5a0e 100644 --- a/internal/provider/waypoint/resource_waypoint_application_template.go +++ b/internal/provider/waypoint/resource_waypoint_application_template.go @@ -128,7 +128,7 @@ func (r *ApplicationTemplateResource) Schema(ctx context.Context, req resource.S }, "terraform_project_id": &schema.StringAttribute{ Required: true, - Description: "Tetraform Cloud Project ID", + Description: "Terraform Cloud Project ID", }, }, }, @@ -247,7 +247,7 @@ func (r *ApplicationTemplateResource) Create(ctx context.Context, req resource.C appTemplate = app.Payload.ApplicationTemplate } if appTemplate == nil { - resp.Diagnostics.AddError("unknown error creating application template", "empty application template found") + resp.Diagnostics.AddError("unknown error creating application template", "empty application template returned") return } @@ -467,7 +467,7 @@ func (r *ApplicationTemplateResource) Update(ctx context.Context, req resource.U appTemplate = app.Payload.ApplicationTemplate } if appTemplate == nil { - resp.Diagnostics.AddError("unknown error updating application template", "empty application template found") + resp.Diagnostics.AddError("unknown error updating application template", "empty application template returned") return } From 5d72023a01b062e6b3765820fda700bce12c3d50 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 10:05:59 -0500 Subject: [PATCH 05/17] basics of application and docs --- docs/resources/waypoint_application.md | 4 +- .../waypoint/resource_waypoint_application.go | 310 ++++++++---------- ...urce_waypoint_application_template_test.go | 3 +- .../resource_waypoint_application_test.go | 42 ++- 4 files changed, 174 insertions(+), 185 deletions(-) diff --git a/docs/resources/waypoint_application.md b/docs/resources/waypoint_application.md index f386c07ea..9b1d0df3b 100644 --- a/docs/resources/waypoint_application.md +++ b/docs/resources/waypoint_application.md @@ -22,12 +22,12 @@ Waypoint Application resource ### Optional -- `application_template_name` (String) Name of the Application Template this Application is based on. - `project_id` (String) The ID of the HCP project where the Waypoint Application is located. -- `readme_markdown` (String) Instructions for using the Application (markdown format supported) +- `readme_markdown` (String) Instructions for using the Application (markdownformat supported). Note: this is a base64 encoded string, andcan only be set in configuration after initial creation. Theinitial version of the README is generated from the READMETemplate from source Application Template. ### Read-Only +- `application_template_name` (String) Name of the Application Template this Application is based on. - `id` (String) The ID of the Application. - `namespace_id` (String) Internal Namespace ID. - `organization_id` (String) The ID of the HCP organization where the Waypoint Application is located. diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index 7384ea514..e799c0d97 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -5,6 +5,7 @@ package waypoint import ( "context" + "encoding/base64" "fmt" sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" @@ -46,10 +47,8 @@ type ApplicationResourceModel struct { // deferred for now // Tags types.List `tfsdk:"tags"` - // CreatedAt types.String `tfsdk:"created_at"` - // UpdatedAt types.String `tfsdk:"updated_at"` - // deferred and probably a list or objects + // deferred and probably a list or objects, but may possible be a separate // ActionCfgs types.List `tfsdk:"action_cfgs"` } @@ -73,6 +72,9 @@ func (r *ApplicationResource) Schema(ctx context.Context, req resource.SchemaReq "name": schema.StringAttribute{ Description: "The name of the Application.", Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "organization_id": schema.StringAttribute{ Description: "The ID of the HCP organization where the Waypoint Application is located.", @@ -93,20 +95,33 @@ func (r *ApplicationResource) Schema(ctx context.Context, req resource.SchemaReq Required: true, Description: "ID of the Application Template this Application is based on.", }, - // TODO(clint): not sure why both are in the model + // application_template_name is a computed only attribute for ease + // of reference "application_template_name": schema.StringAttribute{ Computed: true, - Optional: true, Description: "Name of the Application Template this Application is based on.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "readme_markdown": schema.StringAttribute{ - Computed: true, - Optional: true, - Description: "Instructions for using the Application (markdown format supported)", + Computed: true, + Optional: true, + Description: "Instructions for using the Application (markdown" + + "format supported). Note: this is a base64 encoded string, and" + + "can only be set in configuration after initial creation. The" + + "initial version of the README is generated from the README" + + "Template from source Application Template.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "namespace_id": schema.StringAttribute{ Computed: true, Description: "Internal Namespace ID.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } @@ -165,8 +180,7 @@ func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateReq modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceCreateApplicationFromTemplateBody{ Name: plan.Name.ValueString(), ApplicationTemplate: &waypoint_models.HashicorpCloudWaypointRefApplicationTemplate{ - // ID: plan.ApplicationTemplateID.ValueString(), - Name: plan.ApplicationTemplateName.ValueString(), + ID: plan.ApplicationTemplateID.ValueString(), }, } @@ -185,7 +199,7 @@ func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateReq application = app.Payload.Application } if application == nil { - resp.Diagnostics.AddError("unknown error creating application from template", "empty application template returned") + resp.Diagnostics.AddError("unknown error creating application from template", "empty application returned") return } @@ -193,6 +207,7 @@ func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateReq plan.ProjectID = types.StringValue(projectID) plan.Name = types.StringValue(application.Name) plan.OrgID = types.StringValue(orgID) + plan.ApplicationTemplateName = types.StringValue(application.ApplicationTemplate.Name) plan.NamespaceID = types.StringValue(ns.ID) // set plan.readme if it's not null or appTemplate.readme is not @@ -236,11 +251,11 @@ func (r *ApplicationResource) Read(ctx context.Context, req resource.ReadRequest application, err := clients.GetApplicationByID(ctx, client, loc, data.ID.ValueString()) if err != nil { if clients.IsResponseCodeNotFound(err) { - tflog.Info(ctx, "TFC Config not found for organization, removing from state.") + tflog.Info(ctx, "Application not found for organization, removing from state.") resp.State.RemoveResource(ctx) return } - resp.Diagnostics.AddError("Error reading TFC Config", err.Error()) + resp.Diagnostics.AddError("Error reading Application", err.Error()) return } @@ -248,7 +263,7 @@ func (r *ApplicationResource) Read(ctx context.Context, req resource.ReadRequest data.ProjectID = types.StringValue(projectID) data.Name = types.StringValue(application.Name) data.OrgID = types.StringValue(orgID) - // data.NamespaceID = types.StringValue(ns.ID) + data.ApplicationTemplateName = types.StringValue(application.ApplicationTemplate.Name) // set plan.readme if it's not null or appTemplate.readme is not // empty @@ -270,122 +285,87 @@ func (r *ApplicationResource) Update(ctx context.Context, req resource.UpdateReq return } - // projectID := r.client.Config.ProjectID - // if !plan.ProjectID.IsUnknown() { - // projectID = plan.ProjectID.ValueString() - // } - - // orgID := r.client.Config.OrganizationID - // loc := &sharedmodels.HashicorpCloudLocationLocation{ - // OrganizationID: orgID, - // ProjectID: projectID, - // } - - // client := r.client - // ns, err := getNamespaceByLocation(ctx, client, loc) - // if err != nil { - // resp.Diagnostics.AddError( - // "error getting namespace by location", - // err.Error(), - // ) - // return - // } - - // strLabels := []string{} - // diags := plan.Labels.ElementsAs(ctx, &strLabels, false) - // if diags.HasError() { - // return - // } - - // readmeBytes, err := base64.StdEncoding.DecodeString(plan.ReadmeMarkdown.ValueString()) - // if err != nil { - // resp.Diagnostics.AddError( - // "error decoding the base64 file contents", - // err.Error(), - // ) - // return - // } - - // modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceUpdateApplicationTemplateBody{ - // ApplicationTemplate: &waypoint_models.HashicorpCloudWaypointApplicationTemplate{ - // Name: plan.Name.ValueString(), - // Summary: plan.Summary.ValueString(), - // Labels: strLabels, - // Description: plan.Description.ValueString(), - // ReadmeMarkdownTemplate: readmeBytes, - // TerraformNocodeModule: &waypoint_models.HashicorpCloudWaypointTerraformNocodeModule{ - // Source: plan.TerraformNoCodeModule.Source.ValueString(), - // Version: plan.TerraformNoCodeModule.Version.ValueString(), - // }, - // TerraformCloudWorkspaceDetails: &waypoint_models.HashicorpCloudWaypointTerraformCloudWorkspaceDetails{ - // Name: plan.TerraformCloudWorkspace.Name.ValueString(), - // ProjectID: plan.TerraformCloudWorkspace.TerraformProjectID.ValueString(), - // }, - // }, - // } - - // params := &waypoint_service.WaypointServiceUpdateApplicationTemplateParams{ - // NamespaceID: ns.ID, - // Body: modelBody, - // ExistingApplicationTemplateID: plan.ID.ValueString(), - // } - // app, err := r.client.Waypoint.WaypointServiceUpdateApplicationTemplate(params, nil) - // if err != nil { - // resp.Diagnostics.AddError("Error updating project", err.Error()) - // return - // } - - // var appTemplate *waypoint_models.HashicorpCloudWaypointApplicationTemplate - // if app.Payload != nil { - // appTemplate = app.Payload.ApplicationTemplate - // } - // if appTemplate == nil { - // resp.Diagnostics.AddError("unknown error updating application - // template", "empty application template returned") - // return - // } - - // plan.ID = types.StringValue(appTemplate.ID) - // plan.ProjectID = types.StringValue(projectID) - // plan.Name = types.StringValue(appTemplate.Name) - // plan.OrgID = types.StringValue(orgID) - // plan.Summary = types.StringValue(appTemplate.Summary) - - // labels, diags := types.ListValueFrom(ctx, types.StringType, appTemplate.Labels) - // resp.Diagnostics.Append(diags...) - // if resp.Diagnostics.HasError() { - // return - // } - // plan.Labels = labels - - // if appTemplate.TerraformCloudWorkspaceDetails != nil { - // tfcWorkspace := &tfcWorkspace{ - // Name: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.Name), - // TerraformProjectID: types.StringValue(appTemplate.TerraformCloudWorkspaceDetails.ProjectID), - // } - // plan.TerraformCloudWorkspace = tfcWorkspace - // } - - // if appTemplate.TerraformNocodeModule != nil { - // tfcNoCode := &tfcNoCodeModule{ - // Source: types.StringValue(appTemplate.TerraformNocodeModule.Source), - // Version: types.StringValue(appTemplate.TerraformNocodeModule.Version), - // } - // plan.TerraformNoCodeModule = tfcNoCode - // } - - // plan.Description = types.StringValue(appTemplate.Description) - // if appTemplate.Description == "" { - // plan.Description = types.StringNull() - // } - // plan.ReadmeMarkdown = types.StringValue(appTemplate.ReadmeMarkdownTemplate.String()) - // if appTemplate.ReadmeMarkdownTemplate.String() == "" { - // plan.ReadmeMarkdown = types.StringNull() - // } + // get the current state as well, so we know the current name of the + // application for reference during the update + var data *ApplicationResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + projectID := r.client.Config.ProjectID + if !plan.ProjectID.IsUnknown() { + projectID = plan.ProjectID.ValueString() + } + + orgID := r.client.Config.OrganizationID + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: orgID, + ProjectID: projectID, + } + + client := r.client + ns, err := getNamespaceByLocation(ctx, client, loc) + if err != nil { + resp.Diagnostics.AddError( + "error getting namespace by location", + err.Error(), + ) + return + } + + // read the readme from the plan and decode it + readmeBytes, err := base64.StdEncoding.DecodeString(plan.ReadmeMarkdown.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "error decoding the base64 file contents", + err.Error(), + ) + return + } + + modelBody := &waypoint_models.HashicorpCloudWaypointWaypointServiceUpdateApplicationBody{ + // this is the updated name + Name: plan.Name.ValueString(), + ReadmeMarkdown: readmeBytes, + } + + params := &waypoint_service.WaypointServiceUpdateApplicationParams{ + ApplicationID: plan.ID.ValueString(), + NamespaceID: ns.ID, + Body: modelBody, + } + app, err := r.client.Waypoint.WaypointServiceUpdateApplication(params, nil) + if err != nil { + resp.Diagnostics.AddError("Error updating Application", err.Error()) + return + } + + var application *waypoint_models.HashicorpCloudWaypointApplication + if app.Payload != nil { + application = app.Payload.Application + } + if application == nil { + resp.Diagnostics.AddError("unknown error updating application", "empty application returned") + return + } + + plan.ID = types.StringValue(application.ID) + plan.ProjectID = types.StringValue(projectID) + plan.Name = types.StringValue(application.Name) + plan.OrgID = types.StringValue(orgID) + plan.ApplicationTemplateName = types.StringValue(application.ApplicationTemplate.Name) + plan.NamespaceID = types.StringValue(ns.ID) + + // set plan.readme if it's not null or appTemplate.readme is not + // empty + plan.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) + if application.ReadmeMarkdown.String() == "" { + plan.ReadmeMarkdown = types.StringNull() + } // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log - tflog.Trace(ctx, "updated application template resource") + tflog.Trace(ctx, "updated application resource") // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -401,44 +381,44 @@ func (r *ApplicationResource) Delete(ctx context.Context, req resource.DeleteReq return } - // projectID := r.client.Config.ProjectID - // if !data.ProjectID.IsUnknown() { - // projectID = data.ProjectID.ValueString() - // } - - // loc := &sharedmodels.HashicorpCloudLocationLocation{ - // OrganizationID: r.client.Config.OrganizationID, - // ProjectID: projectID, - // } - - // client := r.client - // ns, err := getNamespaceByLocation(ctx, client, loc) - // if err != nil { - // resp.Diagnostics.AddError( - // "Error Deleting TFC Config", - // err.Error(), - // ) - // return - // } - - // params := &waypoint_service.WaypointServiceDeleteApplicationTemplateParams{ - // NamespaceID: ns.ID, - // ApplicationTemplateID: data.ID.ValueString(), - // } - - // _, err = r.client.Waypoint.WaypointServiceDeleteApplicationTemplate(params, nil) - - // if err != nil { - // if clients.IsResponseCodeNotFound(err) { - // tflog.Info(ctx, "Application Template not found for organization during delete call, ignoring") - // return - // } - // resp.Diagnostics.AddError( - // "Error Deleting Application Template", - // err.Error(), - // ) - // return - // } + projectID := r.client.Config.ProjectID + if !data.ProjectID.IsUnknown() { + projectID = data.ProjectID.ValueString() + } + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: r.client.Config.OrganizationID, + ProjectID: projectID, + } + + client := r.client + ns, err := getNamespaceByLocation(ctx, client, loc) + if err != nil { + resp.Diagnostics.AddError( + "error deleting application", + err.Error(), + ) + return + } + + params := &waypoint_service.WaypointServiceDestroyApplicationParams{ + NamespaceID: ns.ID, + ApplicationID: data.ID.ValueString(), + } + + _, err = r.client.Waypoint.WaypointServiceDestroyApplication(params, nil) + + if err != nil { + if clients.IsResponseCodeNotFound(err) { + tflog.Info(ctx, "Application not found for organization during delete call, ignoring") + return + } + resp.Diagnostics.AddError( + "error deleting Application", + err.Error(), + ) + return + } } func (r *ApplicationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/internal/provider/waypoint/resource_waypoint_application_template_test.go b/internal/provider/waypoint/resource_waypoint_application_template_test.go index ff205134c..9b1710fc9 100644 --- a/internal/provider/waypoint/resource_waypoint_application_template_test.go +++ b/internal/provider/waypoint/resource_waypoint_application_template_test.go @@ -132,8 +132,9 @@ func testAccCheckWaypointAppTemplateDestroy(t *testing.T, appTemplateModel *wayp func testAppTemplateConfig(name string) string { return fmt.Sprintf(` resource "hcp_waypoint_application_template" "test" { - name = %q + name = "%s" summary = "some summary for fun" + readme_markdown_template = base64encode("# Some Readme") terraform_no_code_module = { source = "some source" version = "some version" diff --git a/internal/provider/waypoint/resource_waypoint_application_test.go b/internal/provider/waypoint/resource_waypoint_application_test.go index 1b719bf78..f16e1d923 100644 --- a/internal/provider/waypoint/resource_waypoint_application_test.go +++ b/internal/provider/waypoint/resource_waypoint_application_test.go @@ -20,8 +20,8 @@ import ( func TestAccWaypoint_Application_basic(t *testing.T) { var applicationModel waypoint.ApplicationResourceModel resourceName := "hcp_waypoint_application.test" - name := generateRandomName() - updatedName := generateRandomName() + templateName := generateRandomName() + applicationName := generateRandomName() resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -29,19 +29,11 @@ func TestAccWaypoint_Application_basic(t *testing.T) { CheckDestroy: testAccCheckWaypointApplicationDestroy(t, &applicationModel), Steps: []resource.TestStep{ { - Config: testApplicationConfig(name), + Config: testApplicationConfig(templateName, applicationName), Check: resource.ComposeTestCheckFunc( testAccCheckWaypointApplicationExists(t, resourceName, &applicationModel), - testAccCheckWaypointApplicationName(t, &applicationModel, name), - resource.TestCheckResourceAttr(resourceName, "name", name), - ), - }, - { - Config: testApplicationConfig(updatedName), - Check: resource.ComposeTestCheckFunc( - testAccCheckWaypointApplicationExists(t, resourceName, &applicationModel), - testAccCheckWaypointApplicationName(t, &applicationModel, updatedName), - resource.TestCheckResourceAttr(resourceName, "name", updatedName), + testAccCheckWaypointApplicationName(t, &applicationModel, applicationName), + resource.TestCheckResourceAttr(resourceName, "name", applicationName), ), }, }, @@ -126,12 +118,28 @@ func testAccCheckWaypointApplicationDestroy(t *testing.T, applicationModel *wayp } } -func testApplicationConfig(name string) string { +// these are hardcoded project and no-code module values because they work. The +// automated tests do not run acceptance tests at this time, so these should be +// sufficient for now. +func testApplicationConfig(tempName, appName string) string { return fmt.Sprintf(` -%s +resource "hcp_waypoint_application_template" "test" { + name = "%s" + summary = "some summary for fun" + readme_markdown_template = base64encode("# Some Readme") + terraform_no_code_module = { + source = "private/waypoint-tfc-testing/waypoint-template-starter/null" + version = "0.0.2" + } + terraform_cloud_workspace_details = { + name = "Default Project" + terraform_project_id = "prj-gfVyPJ2q2Aurn25o" + } + labels = ["one", "two"] +} resource "hcp_waypoint_application" "test" { - name = %q + name = "%s" application_template_id = hcp_waypoint_application_template.test.id -}`, testAppTemplateConfig(name), name) +}`, tempName, appName) } From 91d3e138b56b9ec156bb7aa2d7a22c53dddfeb91 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 10:27:34 -0500 Subject: [PATCH 06/17] add application data-source and docs --- docs/data-sources/waypoint_application.md | 30 ++++ internal/provider/provider.go | 1 + .../data_source_waypoint_application.go | 164 ++++++++++++++++++ .../data_source_waypoint_application_test.go | 53 ++++++ 4 files changed, 248 insertions(+) create mode 100644 docs/data-sources/waypoint_application.md create mode 100644 internal/provider/waypoint/data_source_waypoint_application.go create mode 100644 internal/provider/waypoint/data_source_waypoint_application_test.go diff --git a/docs/data-sources/waypoint_application.md b/docs/data-sources/waypoint_application.md new file mode 100644 index 000000000..0e19905eb --- /dev/null +++ b/docs/data-sources/waypoint_application.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_waypoint_application Data Source - terraform-provider-hcp" +subcategory: "" +description: |- + The Waypoint Application data source retrieves information on a given Application. +--- + +# hcp_waypoint_application (Data Source) + +The Waypoint Application data source retrieves information on a given Application. + + + + +## Schema + +### Optional + +- `id` (String) The ID of the Application. +- `name` (String) The name of the Application. +- `project_id` (String) The ID of the HCP project where the Waypoint Application is located. + +### Read-Only + +- `application_template_id` (String) ID of the Application Template this Application is based on. +- `application_template_name` (String) Name of the Application Template this Application is based on. +- `namespace_id` (String) Internal Namespace ID. +- `organization_id` (String) The ID of the HCP organization where the Waypoint Application is located. +- `readme_markdown` (String) Instructions for using the Application (markdown format supported diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8ed2af135..f26382282 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -172,6 +172,7 @@ func (p *ProviderFramework) DataSources(ctx context.Context) []func() datasource iam.NewGroupDataSource, iam.NewUserPrincipalDataSource, // Waypoint + waypoint.NewApplicationDataSource, waypoint.NewApplicationTemplateDataSource, waypoint.NewAddOnDefinitionDataSource, }, packer.DataSourceSchemaBuilders...) diff --git a/internal/provider/waypoint/data_source_waypoint_application.go b/internal/provider/waypoint/data_source_waypoint_application.go new file mode 100644 index 000000000..6558d094b --- /dev/null +++ b/internal/provider/waypoint/data_source_waypoint_application.go @@ -0,0 +1,164 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package waypoint + +import ( + "context" + "fmt" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + waypoint_models "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/models" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +var _ datasource.DataSource = &DataSourceApplication{} +var _ datasource.DataSourceWithConfigValidators = &DataSourceApplication{} + +func (d DataSourceApplication) ConfigValidators(ctx context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.Conflicting( + path.MatchRoot("name"), + path.MatchRoot("id"), + ), + } +} + +type DataSourceApplication struct { + client *clients.Client +} + +// type DataSourceApplicationModel struct { +// ID types.String `tfsdk:"id"` +// Name types.String `tfsdk:"name"` +// ProjectID types.String `tfsdk:"project_id"` +// OrgID types.String `tfsdk:"organization_id"` +// Summary types.String `tfsdk:"summary"` +// Labels types.List `tfsdk:"labels"` +// Description types.String `tfsdk:"description"` +// ReadmeMarkdown types.String `tfsdk:"readme_markdown_template"` + +// TerraformCloudWorkspace *tfcWorkspace `tfsdk:"terraform_cloud_workspace_details"` +// TerraformNoCodeModule *tfcNoCodeModule `tfsdk:"terraform_no_code_module"` +// } + +func NewApplicationDataSource() datasource.DataSource { + return &DataSourceApplication{} +} + +func (d *DataSourceApplication) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_waypoint_application" +} + +func (d *DataSourceApplication) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "The Waypoint Application data source retrieves information on a given Application.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Optional: true, + Description: "The ID of the Application.", + }, + "name": schema.StringAttribute{ + Description: "The name of the Application.", + Computed: true, + Optional: true, + }, + "organization_id": schema.StringAttribute{ + Description: "The ID of the HCP organization where the Waypoint Application is located.", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the HCP project where the Waypoint Application is located.", + Optional: true, + Computed: true, + }, + "readme_markdown": schema.StringAttribute{ + Computed: true, + Description: "Instructions for using the Application (markdown format supported", + }, + "application_template_id": schema.StringAttribute{ + Computed: true, + Description: "ID of the Application Template this Application is based on.", + }, + "application_template_name": schema.StringAttribute{ + Computed: true, + Description: "Name of the Application Template this Application is based on.", + }, + "namespace_id": schema.StringAttribute{ + Computed: true, + Description: "Internal Namespace ID.", + }, + }, + } +} + +func (d *DataSourceApplication) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*clients.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + d.client = client +} + +func (d *DataSourceApplication) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data ApplicationResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + client := d.client + if d.client == nil { + resp.Diagnostics.AddError( + "Unconfigured HCP Client", + "Expected configured HCP client. Please report this issue to the provider developers.", + ) + return + } + + projectID := client.Config.ProjectID + if !data.ProjectID.IsNull() { + projectID = data.ProjectID.ValueString() + } + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: projectID, + } + + var application *waypoint_models.HashicorpCloudWaypointApplication + var err error + + if data.ID.IsNull() { + application, err = clients.GetApplicationByName(ctx, client, loc, data.Name.ValueString()) + } else if data.Name.IsNull() { + application, err = clients.GetApplicationByID(ctx, client, loc, data.ID.ValueString()) + } + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + data.ID = types.StringValue(application.ID) + data.OrgID = types.StringValue(client.Config.OrganizationID) + data.ProjectID = types.StringValue(client.Config.ProjectID) + // set data.readme if it's not null or application.readme is not + // empty + data.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) + if application.ReadmeMarkdown.String() == "" { + data.ReadmeMarkdown = types.StringNull() + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/waypoint/data_source_waypoint_application_test.go b/internal/provider/waypoint/data_source_waypoint_application_test.go new file mode 100644 index 000000000..f61249e4f --- /dev/null +++ b/internal/provider/waypoint/data_source_waypoint_application_test.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package waypoint_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/waypoint" +) + +func TestAccWaypointData_Application_basic(t *testing.T) { + // this is only used to verify the app template gets cleaned up in the end + // of the test, and not used for any other purpose at this time + var applicationModel waypoint.ApplicationResourceModel + resourceName := "hcp_waypoint_application.test" + dataSourceName := "data." + resourceName + templateName := generateRandomName() + applicationName := generateRandomName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: testAccCheckWaypointApplicationDestroy(t, &applicationModel), + Steps: []resource.TestStep{ + { + // establish the base app template and application + Config: testApplicationConfig(templateName, applicationName), + Check: resource.ComposeTestCheckFunc( + testAccCheckWaypointApplicationExists(t, resourceName, &applicationModel), + ), + }, + { + // add a data source config to read the app template + Config: testDataApplicationConfig(templateName, applicationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "name", applicationName), + ), + }, + }, + }) +} + +func testDataApplicationConfig(templateName, applicationName string) string { + return fmt.Sprintf(`%s + +data "hcp_waypoint_application" "test" { + name = hcp_waypoint_application.test.name +}`, testApplicationConfig(templateName, applicationName)) +} From f9dc56d8a9e4f39079e6be2306a040af00ce09f0 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 11:04:47 -0500 Subject: [PATCH 07/17] remove unused model --- .../waypoint/data_source_waypoint_application.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/internal/provider/waypoint/data_source_waypoint_application.go b/internal/provider/waypoint/data_source_waypoint_application.go index 6558d094b..c58593445 100644 --- a/internal/provider/waypoint/data_source_waypoint_application.go +++ b/internal/provider/waypoint/data_source_waypoint_application.go @@ -33,20 +33,6 @@ type DataSourceApplication struct { client *clients.Client } -// type DataSourceApplicationModel struct { -// ID types.String `tfsdk:"id"` -// Name types.String `tfsdk:"name"` -// ProjectID types.String `tfsdk:"project_id"` -// OrgID types.String `tfsdk:"organization_id"` -// Summary types.String `tfsdk:"summary"` -// Labels types.List `tfsdk:"labels"` -// Description types.String `tfsdk:"description"` -// ReadmeMarkdown types.String `tfsdk:"readme_markdown_template"` - -// TerraformCloudWorkspace *tfcWorkspace `tfsdk:"terraform_cloud_workspace_details"` -// TerraformNoCodeModule *tfcNoCodeModule `tfsdk:"terraform_no_code_module"` -// } - func NewApplicationDataSource() datasource.DataSource { return &DataSourceApplication{} } From 28a20a7bda9e55b794549ac18f0cb756b84f5805 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 11:11:20 -0500 Subject: [PATCH 08/17] changelog --- .changelog/794.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changelog/794.txt diff --git a/.changelog/794.txt b/.changelog/794.txt new file mode 100644 index 000000000..a911dcbc1 --- /dev/null +++ b/.changelog/794.txt @@ -0,0 +1,4 @@ +```release-note:feature + New resource: Add `hcp_waypoint_application` resource for managing Waypoint Application. + New data-source: Add `data.hcp_waypoint_application` data-source for Waypoint Application. + ``` From 98d7fbedf7e6cfc49f885969d4c5e65085300941 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 11:17:49 -0500 Subject: [PATCH 09/17] rename test so data source test can be ran in same invocation --- .../provider/waypoint/data_source_waypoint_application_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/waypoint/data_source_waypoint_application_test.go b/internal/provider/waypoint/data_source_waypoint_application_test.go index f61249e4f..c5fdb9916 100644 --- a/internal/provider/waypoint/data_source_waypoint_application_test.go +++ b/internal/provider/waypoint/data_source_waypoint_application_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-provider-hcp/internal/provider/waypoint" ) -func TestAccWaypointData_Application_basic(t *testing.T) { +func TestAccWaypoint_Application_DataSource_basic(t *testing.T) { // this is only used to verify the app template gets cleaned up in the end // of the test, and not used for any other purpose at this time var applicationModel waypoint.ApplicationResourceModel From a30d41e59365ccd18a52a4c050e9dda544e86d73 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 11:23:07 -0500 Subject: [PATCH 10/17] cleanups --- docs/data-sources/waypoint_application.md | 2 +- .../provider/waypoint/data_source_waypoint_application.go | 2 +- internal/provider/waypoint/resource_waypoint_application.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/data-sources/waypoint_application.md b/docs/data-sources/waypoint_application.md index 0e19905eb..740c3ab89 100644 --- a/docs/data-sources/waypoint_application.md +++ b/docs/data-sources/waypoint_application.md @@ -27,4 +27,4 @@ The Waypoint Application data source retrieves information on a given Applicatio - `application_template_name` (String) Name of the Application Template this Application is based on. - `namespace_id` (String) Internal Namespace ID. - `organization_id` (String) The ID of the HCP organization where the Waypoint Application is located. -- `readme_markdown` (String) Instructions for using the Application (markdown format supported +- `readme_markdown` (String) Instructions for using the Application (markdown format supported). diff --git a/internal/provider/waypoint/data_source_waypoint_application.go b/internal/provider/waypoint/data_source_waypoint_application.go index c58593445..2483d45e8 100644 --- a/internal/provider/waypoint/data_source_waypoint_application.go +++ b/internal/provider/waypoint/data_source_waypoint_application.go @@ -66,7 +66,7 @@ func (d *DataSourceApplication) Schema(ctx context.Context, req datasource.Schem }, "readme_markdown": schema.StringAttribute{ Computed: true, - Description: "Instructions for using the Application (markdown format supported", + Description: "Instructions for using the Application (markdown format supported).", }, "application_template_id": schema.StringAttribute{ Computed: true, diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index e799c0d97..876658a44 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -210,7 +210,7 @@ func (r *ApplicationResource) Create(ctx context.Context, req resource.CreateReq plan.ApplicationTemplateName = types.StringValue(application.ApplicationTemplate.Name) plan.NamespaceID = types.StringValue(ns.ID) - // set plan.readme if it's not null or appTemplate.readme is not + // set plan.readme if it's not null or application.readme is not // empty plan.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) if application.ReadmeMarkdown.String() == "" { @@ -265,7 +265,7 @@ func (r *ApplicationResource) Read(ctx context.Context, req resource.ReadRequest data.OrgID = types.StringValue(orgID) data.ApplicationTemplateName = types.StringValue(application.ApplicationTemplate.Name) - // set plan.readme if it's not null or appTemplate.readme is not + // set plan.readme if it's not null or application.readme is not // empty data.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) if application.ReadmeMarkdown.String() == "" { @@ -356,7 +356,7 @@ func (r *ApplicationResource) Update(ctx context.Context, req resource.UpdateReq plan.ApplicationTemplateName = types.StringValue(application.ApplicationTemplate.Name) plan.NamespaceID = types.StringValue(ns.ID) - // set plan.readme if it's not null or appTemplate.readme is not + // set plan.readme if it's not null or application.readme is not // empty plan.ReadmeMarkdown = types.StringValue(application.ReadmeMarkdown.String()) if application.ReadmeMarkdown.String() == "" { From 94f739183e4533def6a5865ce89d8b9d5fe96adb Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 15:38:05 -0500 Subject: [PATCH 11/17] update application markdown description --- internal/provider/waypoint/resource_waypoint_application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index 876658a44..e29e1aea8 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -59,7 +59,7 @@ func (r *ApplicationResource) Metadata(ctx context.Context, req resource.Metadat func (r *ApplicationResource) 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: "Waypoint Application resource", + MarkdownDescription: "The Waypoint Application resource managed the lifecycle of an Application that's based off of a Template.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ From cf4362dd9a7b5731605f6b2f904571edc7f36c18 Mon Sep 17 00:00:00 2001 From: Clint Date: Thu, 21 Mar 2024 15:41:07 -0500 Subject: [PATCH 12/17] Update docs/resources/waypoint_application.md Co-authored-by: Joe <83741749+paladin-devops@users.noreply.github.com> --- docs/resources/waypoint_application.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/waypoint_application.md b/docs/resources/waypoint_application.md index 9b1d0df3b..504d4703b 100644 --- a/docs/resources/waypoint_application.md +++ b/docs/resources/waypoint_application.md @@ -23,7 +23,7 @@ Waypoint Application resource ### Optional - `project_id` (String) The ID of the HCP project where the Waypoint Application is located. -- `readme_markdown` (String) Instructions for using the Application (markdownformat supported). Note: this is a base64 encoded string, andcan only be set in configuration after initial creation. Theinitial version of the README is generated from the READMETemplate from source Application Template. +- `readme_markdown` (String) Instructions for using the Application (markdown format supported). Note: this is a base64 encoded string, and can only be set in configuration after initial creation. The initial version of the README is generated from the README template from source Application Template. ### Read-Only From d15e35f29965785b3eddd1f83ba5fda73b6cc162 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 15:42:19 -0500 Subject: [PATCH 13/17] update generated docs --- docs/resources/waypoint_application.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/waypoint_application.md b/docs/resources/waypoint_application.md index 504d4703b..9b1d0df3b 100644 --- a/docs/resources/waypoint_application.md +++ b/docs/resources/waypoint_application.md @@ -23,7 +23,7 @@ Waypoint Application resource ### Optional - `project_id` (String) The ID of the HCP project where the Waypoint Application is located. -- `readme_markdown` (String) Instructions for using the Application (markdown format supported). Note: this is a base64 encoded string, and can only be set in configuration after initial creation. The initial version of the README is generated from the README template from source Application Template. +- `readme_markdown` (String) Instructions for using the Application (markdownformat supported). Note: this is a base64 encoded string, andcan only be set in configuration after initial creation. Theinitial version of the README is generated from the READMETemplate from source Application Template. ### Read-Only From 261121eb607c1eb43c691c3d44314c8c1c550482 Mon Sep 17 00:00:00 2001 From: catsby Date: Thu, 21 Mar 2024 16:00:49 -0500 Subject: [PATCH 14/17] updated docs after rebase + make with main --- docs/resources/waypoint_application.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/waypoint_application.md b/docs/resources/waypoint_application.md index 9b1d0df3b..f24252820 100644 --- a/docs/resources/waypoint_application.md +++ b/docs/resources/waypoint_application.md @@ -3,12 +3,12 @@ page_title: "hcp_waypoint_application Resource - terraform-provider-hcp" subcategory: "" description: |- - Waypoint Application resource + The Waypoint Application resource managed the lifecycle of an Application that's based off of a Template. --- # hcp_waypoint_application (Resource) -Waypoint Application resource +The Waypoint Application resource managed the lifecycle of an Application that's based off of a Template. From 5a2fdb7e639f5bec2b970b1dfafc3ec1bda9916d Mon Sep 17 00:00:00 2001 From: Clint Date: Fri, 22 Mar 2024 09:17:59 -0500 Subject: [PATCH 15/17] Update internal/provider/waypoint/resource_waypoint_application.go Co-authored-by: HenryEstberg <11286201+HenryEstberg@users.noreply.github.com> --- internal/provider/waypoint/resource_waypoint_application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index e29e1aea8..a8c2929d6 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -108,7 +108,7 @@ func (r *ApplicationResource) Schema(ctx context.Context, req resource.SchemaReq Computed: true, Optional: true, Description: "Instructions for using the Application (markdown" + - "format supported). Note: this is a base64 encoded string, and" + + " format supported). Note: this is a base64 encoded string, and " + "can only be set in configuration after initial creation. The" + "initial version of the README is generated from the README" + "Template from source Application Template.", From aeab4a5c831ff4500eedc672dcb55cc0709b3753 Mon Sep 17 00:00:00 2001 From: Clint Date: Fri, 22 Mar 2024 09:18:07 -0500 Subject: [PATCH 16/17] Update internal/provider/waypoint/resource_waypoint_application.go Co-authored-by: HenryEstberg <11286201+HenryEstberg@users.noreply.github.com> --- internal/provider/waypoint/resource_waypoint_application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/waypoint/resource_waypoint_application.go b/internal/provider/waypoint/resource_waypoint_application.go index a8c2929d6..92c868071 100644 --- a/internal/provider/waypoint/resource_waypoint_application.go +++ b/internal/provider/waypoint/resource_waypoint_application.go @@ -110,7 +110,7 @@ func (r *ApplicationResource) Schema(ctx context.Context, req resource.SchemaReq Description: "Instructions for using the Application (markdown" + " format supported). Note: this is a base64 encoded string, and " + "can only be set in configuration after initial creation. The" + - "initial version of the README is generated from the README" + + " initial version of the README is generated from the README " + "Template from source Application Template.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), From 3f236167caae09bf9b5449b5441e7116a78624c3 Mon Sep 17 00:00:00 2001 From: catsby Date: Fri, 22 Mar 2024 09:19:24 -0500 Subject: [PATCH 17/17] update docs --- docs/resources/waypoint_application.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/waypoint_application.md b/docs/resources/waypoint_application.md index f24252820..161e407d3 100644 --- a/docs/resources/waypoint_application.md +++ b/docs/resources/waypoint_application.md @@ -23,7 +23,7 @@ The Waypoint Application resource managed the lifecycle of an Application that's ### Optional - `project_id` (String) The ID of the HCP project where the Waypoint Application is located. -- `readme_markdown` (String) Instructions for using the Application (markdownformat supported). Note: this is a base64 encoded string, andcan only be set in configuration after initial creation. Theinitial version of the README is generated from the READMETemplate from source Application Template. +- `readme_markdown` (String) Instructions for using the Application (markdown format supported). Note: this is a base64 encoded string, and can only be set in configuration after initial creation. The initial version of the README is generated from the README Template from source Application Template. ### Read-Only