diff --git a/.changelog/44.txt b/.changelog/44.txt new file mode 100644 index 00000000..0fad1e30 --- /dev/null +++ b/.changelog/44.txt @@ -0,0 +1,3 @@ +```release-note:feature +waypoint: Add `hcp waypoint add-ons definitions` CLI for managing HCP Waypoint add-on definitions. +``` diff --git a/internal/commands/waypoint/add-on/add_on.go b/internal/commands/waypoint/add-on/add_on.go new file mode 100644 index 00000000..8847fd0c --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on.go @@ -0,0 +1,21 @@ +package addon + +import ( + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/heredoc" +) + +func NewCmdAddOn(ctx *cmd.Context) *cmd.Command { + cmd := &cmd.Command{ + Name: "add-ons", + ShortHelp: "Manage HCP Waypoint add-ons and add-on definitions.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons" }} command group lets you +manage HCP Waypoint add-ons and add-on definitions. +`), + } + + cmd.AddChild(NewCmdAddOnDefinition(ctx)) + + return cmd +} diff --git a/internal/commands/waypoint/add-on/add_on_definition.go b/internal/commands/waypoint/add-on/add_on_definition.go new file mode 100644 index 00000000..0c8033c5 --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition.go @@ -0,0 +1,49 @@ +package addon + +import ( + "github.com/hashicorp/hcp/internal/commands/waypoint/opts" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/heredoc" +) + +type AddOnDefinitionOpts struct { + opts.WaypointOpts + + Name string + Summary string + Description string + ReadmeMarkdownTemplateFile string + Labels []string + + TerraformNoCodeModuleSource string + TerraformNoCodeModuleVersion string + TerraformCloudProjectName string + TerraformCloudProjectID string + + // testFunc is used for testing, so that the command can be tested without + // using the real API. + testFunc func(c *cmd.Command, args []string) error +} + +func NewCmdAddOnDefinition(ctx *cmd.Context) *cmd.Command { + opts := &AddOnDefinitionOpts{ + WaypointOpts: opts.New(ctx), + } + + cmd := &cmd.Command{ + Name: "definitions", + ShortHelp: "Manage HCP Waypoint add-on definitions.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons definitions" }} command +group lets you manage HCP Waypoint add-on definitions. +`), + } + + cmd.AddChild(NewCmdAddOnDefinitionCreate(ctx, opts)) + cmd.AddChild(NewCmdAddOnDefinitionDelete(ctx, opts)) + cmd.AddChild(NewCmdAddOnDefinitionList(ctx, opts)) + cmd.AddChild(NewCmdAddOnDefinitionRead(ctx, opts)) + cmd.AddChild(NewCmdAddOnDefinitionUpdate(ctx, opts)) + + return cmd +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_create.go b/internal/commands/waypoint/add-on/add_on_definition_create.go new file mode 100644 index 00000000..a7273328 --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_create.go @@ -0,0 +1,169 @@ +package addon + +import ( + "fmt" + "os" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/client/waypoint_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/models" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/pkg/errors" +) + +func NewCmdAddOnDefinitionCreate(ctx *cmd.Context, opts *AddOnDefinitionOpts) *cmd.Command { + cmd := &cmd.Command{ + Name: "create", + ShortHelp: "Create a new HCP Waypoint add-on definition.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons definitions create" }} +command lets you create HCP Waypoint add-on definitions. +`), + Examples: []cmd.Example{ + { + Preamble: "Create a new HCP Waypoint add-on definition:", + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` +$ hcp waypoint add-ons definitions create -n my-add-on-definition \ + -s "My Add-on Definition summary." \ + -d "My Add-on Definition description." \ + --readme-markdown-template-file "README.tpl" \ + --tfc-no-code-module-source "app.terraform.io/hashicorp/dir/template" \ + --tfc-no-code-module-version "1.0.2" \ + --tfc-project-name "my-tfc-project" \ + --tfc-project-id "prj-123456" \ + -l label1 \ + -l label2 +`), + }, + }, + RunF: func(c *cmd.Command, args []string) error { + if opts.testFunc != nil { + return opts.testFunc(c, args) + } + return addOnDefinitionCreate(opts) + }, + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrgAndProject(ctx) + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "name", + Shorthand: "n", + DisplayValue: "NAME", + Description: "The name of the add-on definition.", + Value: flagvalue.Simple("", &opts.Name), + }, + { + Name: "summary", + Shorthand: "s", + DisplayValue: "SUMMARY", + Description: "The summary of the add-on definition.", + Value: flagvalue.Simple("", &opts.Summary), + }, + { + Name: "description", + Shorthand: "d", + DisplayValue: "DESCRIPTION", + Description: "The description of the add-on definition.", + Value: flagvalue.Simple("", &opts.Description), + }, + { + Name: "readme-markdown-template-file", + DisplayValue: "README_MARKDOWN_TEMPLATE_FILE_PATH", + Description: "The file containing the README markdown template.", + Value: flagvalue.Simple("", &opts.ReadmeMarkdownTemplateFile), + }, + { + Name: "label", + Shorthand: "l", + DisplayValue: "LABEL", + Description: "A label to apply to the add-on definition.", + Repeatable: true, + Value: flagvalue.SimpleSlice(nil, &opts.Labels), + }, + { + Name: "tfc-no-code-module-source", + DisplayValue: "TFC_NO_CODE_MODULE_SOURCE", + Description: heredoc.New(ctx.IO).Must(` + The source of the Terraform no-code module. + The expected format is "NAMESPACE/NAME/PROVIDER". An + optional "HOSTNAME/" can be added at the beginning for + a private registry. + `), + Value: flagvalue.Simple("", &opts.TerraformNoCodeModuleSource), + Required: true, + }, + { + Name: "tfc-no-code-module-version", + DisplayValue: "TFC_NO_CODE_MODULE_VERSION", + Description: "The version of the Terraform no-code module.", + Value: flagvalue.Simple("", &opts.TerraformNoCodeModuleVersion), + Required: true, + }, + { + Name: "tfc-project-name", + DisplayValue: "TFC_PROJECT_NAME", + Description: "The name of the Terraform Cloud project where" + + " applications using this add-on definition will be created.", + Value: flagvalue.Simple("", &opts.TerraformCloudProjectName), + Required: true, + }, + { + Name: "tfc-project-id", + DisplayValue: "TFC_PROJECT_ID", + Description: "The ID of the Terraform Cloud project where" + + " applications using this add-on definition will be created.", + Value: flagvalue.Simple("", &opts.TerraformCloudProjectID), + Required: true, + }, + }, + }, + } + + return cmd +} + +func addOnDefinitionCreate(opts *AddOnDefinitionOpts) error { + ns, err := opts.Namespace() + if err != nil { + return errors.Wrapf(err, "Unable to access HCP project") + } + + var readmeTpl []byte + if opts.ReadmeMarkdownTemplateFile != "" { + readmeTpl, err = os.ReadFile(opts.ReadmeMarkdownTemplateFile) + if err != nil { + return errors.Wrapf(err, "failed to read README markdown template file %q", opts.ReadmeMarkdownTemplateFile) + } + } + + _, err = opts.WS.WaypointServiceCreateAddOnDefinition( + &waypoint_service.WaypointServiceCreateAddOnDefinitionParams{ + NamespaceID: ns.ID, + Body: &models.HashicorpCloudWaypointWaypointServiceCreateAddOnDefinitionBody{ + Name: opts.Name, + Summary: opts.Summary, + Description: opts.Description, + ReadmeMarkdownTemplate: readmeTpl, + Labels: opts.Labels, + TerraformNocodeModule: &models.HashicorpCloudWaypointTerraformNocodeModule{ + Source: opts.TerraformNoCodeModuleSource, + Version: opts.TerraformNoCodeModuleVersion, + }, + TerraformCloudWorkspaceDetails: &models.HashicorpCloudWaypointTerraformCloudWorkspaceDetails{ + Name: opts.TerraformCloudProjectName, + ProjectID: opts.TerraformCloudProjectID, + }, + }, + Context: opts.Ctx, + }, nil) + if err != nil { + return errors.Wrapf(err, "failed to create add-on definition %q", opts.Name) + } + + fmt.Fprintf(opts.IO.Err(), "Add-on definition %q created.", opts.Name) + + return nil +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_create_test.go b/internal/commands/waypoint/add-on/add_on_definition_create_test.go new file mode 100644 index 00000000..ea1cde5c --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_create_test.go @@ -0,0 +1,110 @@ +package addon + +import ( + "context" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/require" +) + +func TestCmdAddOnDefinitionCreate(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *AddOnDefinitionOpts + }{ + { + Name: "No Org", + Profile: profile.TestProfile, + Args: []string{}, + Error: "Organization ID must be configured", + }, + { + Name: "no args", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{}, + Error: "accepts 1 arg(s), received 0", + }, + { + Name: "happy", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{ + "-n=cli-test", + "-s", "An add-on definition created using the CLI.", + "--tfc-project-id", "prj-abcdefghij", + "--tfc-project-name", "test", + "--tfc-no-code-module-source", "private/waypoint/waypoint-nocode-module/null", + "--tfc-no-code-module-version", "0.0.1", + "-l", "cli", + "-d", "An add-on definition created with the CLI.", + "--readme-markdown-template-file", "readme_test.txt", + }, + Expect: &AddOnDefinitionOpts{ + Name: "cli-test", + Summary: "An add-on definition created using the CLI.", + Description: "An add-on definition created with the CLI.", + TerraformCloudProjectID: "prj-abcdefghij", + TerraformCloudProjectName: "test", + TerraformNoCodeModuleSource: "private/waypoint/waypoint-nocode-module/null", + TerraformNoCodeModuleVersion: "0.0.1", + Labels: []string{"cli"}, + ReadmeMarkdownTemplateFile: "readme_test.txt", + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var aodOpts AddOnDefinitionOpts + aodOpts.testFunc = func(c *cmd.Command, args []string) error { + return nil + } + cmd := NewCmdAddOnDefinitionCreate(ctx, &aodOpts) + cmd.SetIO(io) + + cmd.Run(c.Args) + + if c.Expect != nil { + r.NotNil(c.Expect) + + r.Equal(c.Expect.Name, aodOpts.Name) + r.Equal(c.Expect.Description, aodOpts.Description) + r.Equal(c.Expect.Summary, aodOpts.Summary) + r.Equal(c.Expect.TerraformCloudProjectID, aodOpts.TerraformCloudProjectID) + r.Equal(c.Expect.TerraformCloudProjectName, aodOpts.TerraformCloudProjectName) + r.Equal(c.Expect.TerraformNoCodeModuleSource, aodOpts.TerraformNoCodeModuleSource) + r.Equal(c.Expect.TerraformNoCodeModuleVersion, aodOpts.TerraformNoCodeModuleVersion) + r.Equal(c.Expect.ReadmeMarkdownTemplateFile, aodOpts.ReadmeMarkdownTemplateFile) + r.Equal(c.Expect.Labels, aodOpts.Labels) + } + }) + } +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_delete.go b/internal/commands/waypoint/add-on/add_on_definition_delete.go new file mode 100644 index 00000000..0b369030 --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_delete.go @@ -0,0 +1,75 @@ +package addon + +import ( + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/client/waypoint_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/pkg/errors" +) + +func NewCmdAddOnDefinitionDelete(ctx *cmd.Context, opts *AddOnDefinitionOpts) *cmd.Command { + cmd := &cmd.Command{ + Name: "delete", + ShortHelp: "Delete an HCP Waypoint add-on definition.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons definitions delete" }} +command lets you delete an existing HCP Waypoint add-on definition. +`), + Examples: []cmd.Example{ + { + Preamble: "Delete an HCP Waypoint add-on definition:", + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` +$ hcp waypoint add-ons definitions delete -n my-addon-definition +`), + }, + }, + RunF: func(c *cmd.Command, args []string) error { + if opts.testFunc != nil { + return opts.testFunc(c, args) + } + return addOnDefinitionDelete(opts) + }, + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrgAndProject(ctx) + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "name", + Shorthand: "n", + DisplayValue: "NAME", + Description: "The name of the add-on definition to be deleted.", + Value: flagvalue.Simple("", &opts.Name), + Required: true, + }, + }, + }, + } + + return cmd +} + +func addOnDefinitionDelete(opts *AddOnDefinitionOpts) error { + ns, err := opts.Namespace() + if err != nil { + return err + } + + _, err = opts.WS.WaypointServiceDeleteAddOnDefinition2( + &waypoint_service.WaypointServiceDeleteAddOnDefinition2Params{ + NamespaceID: ns.ID, + Context: opts.Ctx, + AddOnDefinitionName: opts.Name, + }, nil, + ) + if err != nil { + return errors.Wrapf(err, "failed to delete add-on definition %q", opts.Name) + } + + fmt.Fprintf(opts.IO.Err(), "Add-on definition %q deleted.", opts.Name) + + return nil +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_delete_test.go b/internal/commands/waypoint/add-on/add_on_definition_delete_test.go new file mode 100644 index 00000000..fe39e9ef --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_delete_test.go @@ -0,0 +1,84 @@ +package addon + +import ( + "context" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/require" +) + +func TestCmdAddOnDefinitionDelete(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *AddOnDefinitionOpts + }{ + { + Name: "No Org", + Profile: profile.TestProfile, + Args: []string{}, + Error: "Organization ID must be configured", + }, + { + Name: "no args", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{}, + Error: "accepts 1 arg(s), received 0", + }, + { + Name: "happy", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{ + "-n=cli-test", + }, + Expect: &AddOnDefinitionOpts{ + Name: "cli-test", + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r := require.New(t) + + io := iostreams.Test() + + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var aodOpts AddOnDefinitionOpts + aodOpts.testFunc = func(c *cmd.Command, args []string) error { + return nil + } + cmd := NewCmdAddOnDefinitionDelete(ctx, &aodOpts) + cmd.SetIO(io) + + cmd.Run(c.Args) + + if c.Expect != nil { + r.Equal(c.Expect.Name, aodOpts.Name) + } + }) + } +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_list.go b/internal/commands/waypoint/add-on/add_on_definition_list.go new file mode 100644 index 00000000..04fe0d3c --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_list.go @@ -0,0 +1,77 @@ +package addon + +import ( + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/client/waypoint_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/models" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/pkg/errors" +) + +func NewCmdAddOnDefinitionList(ctx *cmd.Context, opts *AddOnDefinitionOpts) *cmd.Command { + cmd := &cmd.Command{ + Name: "list", + ShortHelp: "List all known HCP Waypoint add-on definitions.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons definitions list" }} +command lets you list all existing HCP Waypoint add-on definitions. +`), + Examples: []cmd.Example{ + { + Preamble: "List all known HCP Waypoint add-on definitions:", + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` +$ hcp waypoint add-ons definitions list +`), + }, + }, + RunF: func(c *cmd.Command, args []string) error { + if opts.testFunc != nil { + return opts.testFunc(c, args) + } + return addOnDefinitionsList(opts) + }, + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrgAndProject(ctx) + }, + } + + return cmd +} + +func addOnDefinitionsList(opts *AddOnDefinitionOpts) error { + ns, err := opts.Namespace() + if err != nil { + return err + } + + var addOnDefinitions []*models.HashicorpCloudWaypointAddOnDefinition + + listResp, err := opts.WS.WaypointServiceListAddOnDefinitions( + &waypoint_service.WaypointServiceListAddOnDefinitionsParams{ + NamespaceID: ns.ID, + Context: opts.Ctx, + }, nil, + ) + if err != nil { + return errors.Wrap(err, "failed to list add-on definitions") + } + + addOnDefinitions = append(addOnDefinitions, listResp.GetPayload().AddOnDefinitions...) + + for listResp.GetPayload().Pagination.NextPageToken != "" { + listResp, err = opts.WS.WaypointServiceListAddOnDefinitions( + &waypoint_service.WaypointServiceListAddOnDefinitionsParams{ + NamespaceID: ns.ID, + Context: opts.Ctx, + PaginationNextPageToken: &listResp.GetPayload().Pagination.NextPageToken, + }, nil) + if err != nil { + return errors.Wrapf(err, "failed to list paginated add-on definitions") + } + + addOnDefinitions = append(addOnDefinitions, listResp.GetPayload().AddOnDefinitions...) + } + + return opts.Output.Show(addOnDefinitions, format.Pretty) +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_list_test.go b/internal/commands/waypoint/add-on/add_on_definition_list_test.go new file mode 100644 index 00000000..4db62ec6 --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_list_test.go @@ -0,0 +1,60 @@ +package addon + +import ( + "context" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" +) + +func TestNewCmdAddOnDefinitionList(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *AddOnDefinitionOpts + }{ + { + Name: "No Org", + Profile: profile.TestProfile, + Args: []string{}, + Error: "Organization ID must be configured", + }, + // there's no args for the list command right now, but if that changes, + // we should add a test case here + } + + for _, c := range cases { + c := c + + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var tplOpts AddOnDefinitionOpts + tplOpts.testFunc = func(c *cmd.Command, args []string) error { + return nil + } + cmd := NewCmdAddOnDefinitionList(ctx, &tplOpts) + cmd.SetIO(io) + + cmd.Run(c.Args) + }) + } +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_read.go b/internal/commands/waypoint/add-on/add_on_definition_read.go new file mode 100644 index 00000000..be6e3e7c --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_read.go @@ -0,0 +1,73 @@ +package addon + +import ( + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/client/waypoint_service" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/pkg/errors" +) + +func NewCmdAddOnDefinitionRead(ctx *cmd.Context, opts *AddOnDefinitionOpts) *cmd.Command { + cmd := &cmd.Command{ + Name: "read", + ShortHelp: "Read an HCP Waypoint add-on definition.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons definitions read" }} +command lets you read an existing HCP Waypoint add-on definition. +`), + Examples: []cmd.Example{ + { + Preamble: "Read an HCP Waypoint add-on definition:", + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` +$ hcp waypoint add-ons definitions read -n my-addon-definition +`), + }, + }, + RunF: func(c *cmd.Command, args []string) error { + if opts.testFunc != nil { + return opts.testFunc(c, args) + } + return addOnDefinitionRead(opts) + }, + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrgAndProject(ctx) + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "name", + Shorthand: "n", + DisplayValue: "NAME", + Description: "The name of the add-on definition.", + Value: flagvalue.Simple("", &opts.Name), + Required: true, + }, + }, + }, + } + return cmd +} + +func addOnDefinitionRead(opts *AddOnDefinitionOpts) error { + ns, err := opts.Namespace() + if err != nil { + return errors.Wrap(err, "unable to access HCP project") + } + + getResp, err := opts.WS.WaypointServiceGetAddOnDefinition2( + &waypoint_service.WaypointServiceGetAddOnDefinition2Params{ + NamespaceID: ns.ID, + Context: opts.Ctx, + AddOnDefinitionName: opts.Name, + }, nil, + ) + if err != nil { + return errors.Wrapf(err, "failed to get add-on definition %q", opts.Name) + } + + getRespPayload := getResp.GetPayload() + + return opts.Output.Show(getRespPayload.AddOnDefinition, format.Pretty) +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_read_test.go b/internal/commands/waypoint/add-on/add_on_definition_read_test.go new file mode 100644 index 00000000..9b7e1d3a --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_read_test.go @@ -0,0 +1,84 @@ +package addon + +import ( + "context" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/require" +) + +func TestCmdAddOnDefinitionRead(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *AddOnDefinitionOpts + }{ + { + Name: "No Org", + Profile: profile.TestProfile, + Args: []string{}, + Error: "Organization ID must be configured", + }, + { + Name: "no args", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{}, + Error: "accepts 1 arg(s), received 0", + }, + { + Name: "happy", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{ + "-n=cli-test", + }, + Expect: &AddOnDefinitionOpts{ + Name: "cli-test", + }, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r := require.New(t) + + // Create a context. + io := iostreams.Test() + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var aodOpts AddOnDefinitionOpts + aodOpts.testFunc = func(c *cmd.Command, args []string) error { + return nil + } + cmd := NewCmdAddOnDefinitionRead(ctx, &aodOpts) + cmd.SetIO(io) + + cmd.Run(c.Args) + + if c.Expect != nil { + r.Equal(c.Expect.Name, aodOpts.Name) + } + }) + } +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_update.go b/internal/commands/waypoint/add-on/add_on_definition_update.go new file mode 100644 index 00000000..91c9783a --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_update.go @@ -0,0 +1,160 @@ +package addon + +import ( + "fmt" + "os" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/client/waypoint_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-waypoint-service/preview/2023-08-18/models" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/flagvalue" + "github.com/hashicorp/hcp/internal/pkg/heredoc" + "github.com/pkg/errors" +) + +func NewCmdAddOnDefinitionUpdate(ctx *cmd.Context, opts *AddOnDefinitionOpts) *cmd.Command { + cmd := &cmd.Command{ + Name: "update", + ShortHelp: "Update a HCP Waypoint add-on definition.", + LongHelp: heredoc.New(ctx.IO).Must(` +The {{ template "mdCodeOrBold" "hcp waypoint add-ons definitions update" }} +command lets you update an existing HCP Waypoint add-on definition. +`), + Examples: []cmd.Example{ + { + Preamble: "Update a HCP Waypoint add-on definition:", + Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` +$ hcp waypoint add-ons definitions update -n=my-add-on-definition \ + -s "My updated Add-on Definition summary." \ + -d "My updated Add-on Definition description." \ + --readme-markdown-template-file "README.tpl" \ + --tfc-no-code-module-source "app.terraform.io/hashicorp/dir/template" \ + --tfc-no-code-module-version "1.0.2" \ + --tfc-project-name "my-tfc-project" \ + --tfc-project-id "prj-123456" \ + -l label1 \ + -l label2 +`), + }, + }, + RunF: func(c *cmd.Command, args []string) error { + if opts.testFunc != nil { + return opts.testFunc(c, args) + } + return addOnDefinitionUpdate(opts) + }, + PersistentPreRun: func(c *cmd.Command, args []string) error { + return cmd.RequireOrgAndProject(ctx) + }, + Flags: cmd.Flags{ + Local: []*cmd.Flag{ + { + Name: "name", + Shorthand: "n", + DisplayValue: "NAME", + Description: "The name of the add-on definition.", + Value: flagvalue.Simple("", &opts.Name), + Required: true, + }, + { + Name: "summary", + Shorthand: "s", + DisplayValue: "SUMMARY", + Description: "The summary of the add-on definition.", + Value: flagvalue.Simple("", &opts.Summary), + }, + { + Name: "description", + Shorthand: "d", + DisplayValue: "DESCRIPTION", + Description: "The description of the add-on definition.", + Value: flagvalue.Simple("", &opts.Description), + }, + { + Name: "readme-markdown-template-file", + DisplayValue: "README_MARKDOWN_TEMPLATE_FILE", + Description: "The README markdown template file.", + Value: flagvalue.Simple("", &opts.ReadmeMarkdownTemplateFile), + }, + { + Name: "label", + Shorthand: "l", + DisplayValue: "LABEL", + Description: "A label to apply to the add-on definition.", + Repeatable: true, + Value: flagvalue.SimpleSlice(nil, &opts.Labels), + }, + { + Name: "tfc-no-code-module-source", + DisplayValue: "TFC_NO_CODE_MODULE_SOURCE", + Description: "The Terraform Cloud no-code module source.", + Value: flagvalue.Simple("", &opts.TerraformNoCodeModuleSource), + }, + { + Name: "tfc-no-code-module-version", + DisplayValue: "TFC_NO_CODE_MODULE_VERSION", + Description: "The Terraform Cloud no-code module version.", + Value: flagvalue.Simple("", &opts.TerraformNoCodeModuleVersion), + }, + { + Name: "tfc-project-id", + DisplayValue: "TFC_PROJECT_ID", + Description: "The Terraform Cloud project ID.", + Value: flagvalue.Simple("", &opts.TerraformCloudProjectID), + }, + { + Name: "tfc-project-name", + DisplayValue: "TFC_PROJECT_NAME", + Description: "The Terraform Cloud project name.", + Value: flagvalue.Simple("", &opts.TerraformCloudProjectName), + }, + }, + }, + } + + return cmd +} + +func addOnDefinitionUpdate(opts *AddOnDefinitionOpts) error { + ns, err := opts.Namespace() + if err != nil { + return errors.Wrap(err, "unable to access HCP project") + } + + var readmeTpl []byte + if opts.ReadmeMarkdownTemplateFile != "" { + readmeTpl, err = os.ReadFile(opts.ReadmeMarkdownTemplateFile) + if err != nil { + return errors.Wrapf(err, "failed to read README markdown template file %q", opts.ReadmeMarkdownTemplateFile) + } + } + + _, err = opts.WS.WaypointServiceUpdateAddOnDefinition2( + &waypoint_service.WaypointServiceUpdateAddOnDefinition2Params{ + NamespaceID: ns.ID, + Context: opts.Ctx, + ExistingAddOnDefinitionName: opts.Name, + Body: &models.HashicorpCloudWaypointWaypointServiceUpdateAddOnDefinitionBody{ + Summary: opts.Summary, + Description: opts.Description, + ReadmeMarkdownTemplate: readmeTpl, + Labels: opts.Labels, + TerraformNocodeModule: &models.HashicorpCloudWaypointTerraformNocodeModule{ + Source: opts.TerraformNoCodeModuleSource, + Version: opts.TerraformNoCodeModuleVersion, + }, + TerraformCloudWorkspaceDetails: &models.HashicorpCloudWaypointTerraformCloudWorkspaceDetails{ + ProjectID: opts.TerraformCloudProjectID, + Name: opts.TerraformCloudProjectName, + }, + }, + }, nil, + ) + if err != nil { + return errors.Wrapf(err, "failed to update add-on definition %q", opts.Name) + } + + fmt.Fprintf(opts.IO.Err(), "Add-on definition %q updated.", opts.Name) + + return nil +} diff --git a/internal/commands/waypoint/add-on/add_on_definition_update_test.go b/internal/commands/waypoint/add-on/add_on_definition_update_test.go new file mode 100644 index 00000000..54992859 --- /dev/null +++ b/internal/commands/waypoint/add-on/add_on_definition_update_test.go @@ -0,0 +1,109 @@ +package addon + +import ( + "context" + "testing" + + "github.com/go-openapi/runtime/client" + "github.com/hashicorp/hcp/internal/pkg/cmd" + "github.com/hashicorp/hcp/internal/pkg/format" + "github.com/hashicorp/hcp/internal/pkg/iostreams" + "github.com/hashicorp/hcp/internal/pkg/profile" + "github.com/stretchr/testify/require" +) + +func TestCmdAddOnDefinitionUpdate(t *testing.T) { + t.Parallel() + cases := []struct { + Name string + Args []string + Profile func(t *testing.T) *profile.Profile + Error string + Expect *AddOnDefinitionOpts + }{ + { + Name: "No Org", + Profile: profile.TestProfile, + Args: []string{}, + Error: "Organization ID must be configured", + }, + { + Name: "no args", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{}, + Error: "accepts 1 arg(s), received 0", + }, + { + Name: "happy", + Profile: func(t *testing.T) *profile.Profile { + return profile.TestProfile(t).SetOrgID("123") + }, + Args: []string{ + "-n=cli-test", + "-s", "An add-on definition created using the CLI.", + "--tfc-project-id", "prj-abcdefghij", + "--tfc-project-name", "test", + "--tfc-no-code-module-source", "private/waypoint/waypoint-nocode-module/null", + "--tfc-no-code-module-version", "0.0.1", + "-l", "cli", + "-d", "An add-on definition created with the CLI.", + "--readme-markdown-template-file", "readme_test.txt", + }, + Expect: &AddOnDefinitionOpts{ + Name: "cli-test", + Summary: "An add-on definition created using the CLI.", + Description: "An add-on definition created with the CLI.", + TerraformCloudProjectID: "prj-abcdefghij", + TerraformCloudProjectName: "test", + TerraformNoCodeModuleSource: "private/waypoint/waypoint-nocode-module/null", + TerraformNoCodeModuleVersion: "0.0.1", + Labels: []string{"cli"}, + ReadmeMarkdownTemplateFile: "readme_test.txt", + }, + }, + } + + for _, c := range cases { + c := c + + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r := require.New(t) + + io := iostreams.Test() + + ctx := &cmd.Context{ + IO: io, + Profile: c.Profile(t), + Output: format.New(io), + HCP: &client.Runtime{}, + ShutdownCtx: context.Background(), + } + + var aodOpts AddOnDefinitionOpts + aodOpts.testFunc = func(c *cmd.Command, args []string) error { + return nil + } + cmd := NewCmdAddOnDefinitionUpdate(ctx, &aodOpts) + cmd.SetIO(io) + + cmd.Run(c.Args) + + if c.Expect != nil { + r.Equal(c.Expect.Name, aodOpts.Name) + r.Equal(c.Expect.Summary, aodOpts.Summary) + r.Equal(c.Expect.Description, aodOpts.Description) + r.Equal(c.Expect.TerraformCloudProjectID, aodOpts.TerraformCloudProjectID) + r.Equal(c.Expect.TerraformCloudProjectName, aodOpts.TerraformCloudProjectName) + r.Equal(c.Expect.TerraformNoCodeModuleSource, aodOpts.TerraformNoCodeModuleSource) + r.Equal(c.Expect.TerraformNoCodeModuleVersion, aodOpts.TerraformNoCodeModuleVersion) + r.Equal(c.Expect.Labels, aodOpts.Labels) + r.Equal(c.Expect.ReadmeMarkdownTemplateFile, aodOpts.ReadmeMarkdownTemplateFile) + } + }) + + } +} diff --git a/internal/commands/waypoint/template/list.go b/internal/commands/waypoint/template/list.go index 68b5886e..9ca268cb 100644 --- a/internal/commands/waypoint/template/list.go +++ b/internal/commands/waypoint/template/list.go @@ -60,5 +60,6 @@ func listTemplates(opts *TemplateOpts) error { templates = append(templates, resp.GetPayload().ApplicationTemplates...) } + return opts.Output.Show(templates, format.Pretty) } diff --git a/internal/commands/waypoint/waypoint.go b/internal/commands/waypoint/waypoint.go index 7fabf9f6..67f864df 100644 --- a/internal/commands/waypoint/waypoint.go +++ b/internal/commands/waypoint/waypoint.go @@ -2,6 +2,7 @@ package waypoint import ( "github.com/hashicorp/hcp/internal/commands/waypoint/actionconfig" + addon "github.com/hashicorp/hcp/internal/commands/waypoint/add-on" "github.com/hashicorp/hcp/internal/commands/waypoint/agent" "github.com/hashicorp/hcp/internal/commands/waypoint/template" "github.com/hashicorp/hcp/internal/commands/waypoint/tfcconfig" @@ -24,6 +25,7 @@ func NewCmdWaypoint(ctx *cmd.Context) *cmd.Command { cmd.AddChild(actionconfig.NewCmdActionConfig(ctx)) cmd.AddChild(agent.NewCmdAgent(ctx)) cmd.AddChild(template.NewCmdTemplate(ctx)) + cmd.AddChild(addon.NewCmdAddOn(ctx)) return cmd } diff --git a/internal/pkg/format/output.go b/internal/pkg/format/output.go index 1c045eff..49a16981 100644 --- a/internal/pkg/format/output.go +++ b/internal/pkg/format/output.go @@ -78,7 +78,12 @@ func inferFields[T any](payload T, columns []string) []Field { rv = rv.Elem() } + var ret []Field + if rv.Kind() == reflect.Slice { + if rv.Len() == 0 { + return ret + } rv = rv.Index(0) for rv.Kind() == reflect.Pointer { @@ -98,8 +103,6 @@ func inferFields[T any](payload T, columns []string) []Field { st := rv.Type() - var ret []Field - all := len(toField) == 0 if !all {