diff --git a/.cli-generation-checksum b/.cli-generation-checksum new file mode 100644 index 0000000000..40f17dcd65 --- /dev/null +++ b/.cli-generation-checksum @@ -0,0 +1 @@ +9fbeaf01266df55f34074a848c64c5bdbae9fdbb89295add419c4655c55264f3 diff --git a/go.mod b/go.mod index f67658cfbc..5aa868c21d 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/confluentinc/ccloud-sdk-go-v2/networking-privatelink v0.3.0 github.com/confluentinc/ccloud-sdk-go-v2/org v0.9.0 github.com/confluentinc/ccloud-sdk-go-v2/provider-integration v0.2.0 + github.com/confluentinc/ccloud-sdk-go-v2/rtce v0.1.0 github.com/confluentinc/ccloud-sdk-go-v2/service-quota v0.2.0 github.com/confluentinc/ccloud-sdk-go-v2/srcm v0.7.3 github.com/confluentinc/ccloud-sdk-go-v2/sso v0.0.1 diff --git a/go.sum b/go.sum index d8865d2f2f..19b16c6b12 100644 --- a/go.sum +++ b/go.sum @@ -254,6 +254,8 @@ github.com/confluentinc/ccloud-sdk-go-v2/org v0.9.0 h1:FtaqHX0kBTK7fCQK+9SJcOso+ github.com/confluentinc/ccloud-sdk-go-v2/org v0.9.0/go.mod h1:X0uaTYPp+mr19W1R/Z1LuB1ePZJZrH7kxnQckDx6zoc= github.com/confluentinc/ccloud-sdk-go-v2/provider-integration v0.2.0 h1:UN2a+aqYhk95ro+wVLkeB/8W7n+UV2KsE3jNFbbDCSw= github.com/confluentinc/ccloud-sdk-go-v2/provider-integration v0.2.0/go.mod h1:TzompS9F0G6awN5xMC+nguNG8ULElN5UqX2XOBOIPuM= +github.com/confluentinc/ccloud-sdk-go-v2/rtce v0.1.0 h1:OBa2vm09bOG1oojOP1vNj8V7+M2AfUkYP1sRQ+xlRm4= +github.com/confluentinc/ccloud-sdk-go-v2/rtce v0.1.0/go.mod h1:DNLAZ1R59mLfTM66zypmTIz0R2GouLJfPHeyR/id1gw= github.com/confluentinc/ccloud-sdk-go-v2/service-quota v0.2.0 h1:xVSmwdycExze1E2Jta99CaFuMOlL6k6KExOOSY1hSFg= github.com/confluentinc/ccloud-sdk-go-v2/service-quota v0.2.0/go.mod h1:zZWZoGWJuO0Qm4lO6H2KlXMx4OoB/yhD8y6J1ZB/97Q= github.com/confluentinc/ccloud-sdk-go-v2/srcm v0.7.3 h1:ozdDSJHruQIgtxS5hwz8Rp8pUSX0i4h9besp2H9NYzU= diff --git a/internal/command.go b/internal/command.go index 3eccf66ed5..dac414da0f 100644 --- a/internal/command.go +++ b/internal/command.go @@ -40,6 +40,7 @@ import ( "github.com/confluentinc/cli/v4/internal/plugin" "github.com/confluentinc/cli/v4/internal/prompt" providerintegration "github.com/confluentinc/cli/v4/internal/provider-integration" + "github.com/confluentinc/cli/v4/internal/rtce" schemaregistry "github.com/confluentinc/cli/v4/internal/schema-registry" "github.com/confluentinc/cli/v4/internal/secret" servicequota "github.com/confluentinc/cli/v4/internal/service-quota" @@ -131,6 +132,7 @@ func NewConfluentCommand(cfg *config.Config) *cobra.Command { cmd.AddCommand(plugin.New(cfg, prerunner)) cmd.AddCommand(prompt.New(cfg)) cmd.AddCommand(providerintegration.New(prerunner)) + cmd.AddCommand(rtce.New(cfg, prerunner)) cmd.AddCommand(schemaregistry.New(cfg, prerunner)) cmd.AddCommand(secret.New(prerunner, secrets.NewPasswordProtectionPlugin())) cmd.AddCommand(servicequota.New(prerunner)) diff --git a/internal/rtce/command.go b/internal/rtce/command.go new file mode 100644 index 0000000000..2ee3d36002 --- /dev/null +++ b/internal/rtce/command.go @@ -0,0 +1,23 @@ +package rtce + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/config" +) + +func New(cfg *config.Config, prerunner pcmd.PreRunner) *cobra.Command { + cmd := &cobra.Command{ + Use: "rtce", + Short: "Manage Real Time Context Engine.", + } + + cmd.AddCommand( + newRegionCommand(cfg, prerunner), + newRtceTopicCommand(cfg, prerunner), + // cli-tfgen:cli-subcommands + ) + + return cmd +} diff --git a/internal/rtce/command_region.go b/internal/rtce/command_region.go new file mode 100644 index 0000000000..11d137154e --- /dev/null +++ b/internal/rtce/command_region.go @@ -0,0 +1,37 @@ +package rtce + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/config" +) + +type regionCommand struct { + *pcmd.AuthenticatedCLICommand +} + +type regionOut struct { + ID string `human:"ID" serialized:"id"` + Cloud string `human:"Cloud" serialized:"cloud"` + Region string `human:"Region" serialized:"region"` + DisplayName string `human:"Display Name" serialized:"display_name"` +} + +func newRegionCommand(cfg *config.Config, prerunner pcmd.PreRunner) *cobra.Command { //nolint:unparam + cmd := &cobra.Command{ + Use: "region", + Short: "Manage RTCE regions.", + Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireNonAPIKeyCloudLogin}, + } + + c := ®ionCommand{ + AuthenticatedCLICommand: pcmd.NewAuthenticatedCLICommand(cmd, prerunner), + } + + cmd.AddCommand( + c.newListCommand(), + ) + + return cmd +} diff --git a/internal/rtce/command_region_list.go b/internal/rtce/command_region_list.go new file mode 100644 index 0000000000..124dc4ac90 --- /dev/null +++ b/internal/rtce/command_region_list.go @@ -0,0 +1,56 @@ +package rtce + +import ( + "strings" + + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *regionCommand) newListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List RTCE regions.", + Args: cobra.NoArgs, + RunE: c.list, + } + pcmd.AddCloudAwsFlag(cmd) + cmd.Flags().String("region", "", "Filter the results by exact match for region.") + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *regionCommand) list(cmd *cobra.Command, _ []string) error { + cloud, err := cmd.Flags().GetString("cloud") + if err != nil { + return err + } + cloud = strings.ToUpper(cloud) + + region, err := cmd.Flags().GetString("region") + if err != nil { + return err + } + + regions, err := c.V2Client.ListRtceRegions(cloud, region) + if err != nil { + return err + } + + list := output.NewList(cmd) + for _, region := range regions { + out := ®ionOut{ + ID: region.GetId(), + Cloud: region.GetCloud(), + Region: region.GetRegion(), + DisplayName: region.GetDisplayName(), + } + list.Add(out) + } + return list.Print() +} diff --git a/internal/rtce/command_rtce_topic.go b/internal/rtce/command_rtce_topic.go new file mode 100644 index 0000000000..e4846b0f8a --- /dev/null +++ b/internal/rtce/command_rtce_topic.go @@ -0,0 +1,103 @@ +package rtce + +import ( + "github.com/spf13/cobra" + + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/config" + "github.com/confluentinc/cli/v4/pkg/kafka" + "github.com/confluentinc/cli/v4/pkg/output" +) + +type rtceTopicCommand struct { + *pcmd.AuthenticatedCLICommand +} + +type rtceTopicOut struct { + TopicName string `human:"Topic Name" serialized:"topic_name"` + Cloud string `human:"Cloud" serialized:"cloud"` + Description string `human:"Description" serialized:"description"` + Environment string `human:"Environment" serialized:"environment"` + KafkaCluster string `human:"Kafka Cluster" serialized:"kafka_cluster"` + Region string `human:"Region" serialized:"region"` + ErrorMessage string `human:"Error Message" serialized:"error_message"` + Phase string `human:"Phase" serialized:"phase"` +} + +func newRtceTopicCommand(cfg *config.Config, prerunner pcmd.PreRunner) *cobra.Command { //nolint:unparam + cmd := &cobra.Command{ + Use: "rtce-topic", + Short: "Manage RTCE topics.", + Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireNonAPIKeyCloudLogin}, + } + + c := &rtceTopicCommand{ + AuthenticatedCLICommand: pcmd.NewAuthenticatedCLICommand(cmd, prerunner), + } + + cmd.AddCommand( + c.newCreateCommand(), + c.newDeleteCommand(), + c.newDescribeCommand(), + c.newListCommand(), + c.newUpdateCommand(), + ) + + return cmd +} + +func printRtceTopic(cmd *cobra.Command, rtceTopic rtcev1.RtceV1RtceTopic) error { + table := output.NewTable(cmd) + out := &rtceTopicOut{ + TopicName: rtceTopic.Spec.GetTopicName(), + Cloud: rtceTopic.Spec.GetCloud(), + Description: rtceTopic.Spec.GetDescription(), + Environment: rtceTopic.Spec.Environment.GetId(), + KafkaCluster: rtceTopic.Spec.KafkaCluster.GetId(), + Region: rtceTopic.Spec.GetRegion(), + ErrorMessage: rtceTopic.Status.GetErrorMessage(), + Phase: rtceTopic.Status.GetPhase(), + } + table.Add(out) + return table.Print() +} + +func (c *rtceTopicCommand) validArgs(cmd *cobra.Command, args []string) []string { + if len(args) > 0 { + return nil + } + + return c.validArgsMultiple(cmd, args) +} + +func (c *rtceTopicCommand) validArgsMultiple(cmd *cobra.Command, args []string) []string { + if err := c.PersistentPreRunE(cmd, args); err != nil { + return nil + } + + return c.autocompleteRtceTopics() +} + +func (c *rtceTopicCommand) autocompleteRtceTopics() []string { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return nil + } + kafkaClusterConfig, err := kafka.GetClusterForCommand(c.V2Client, c.Context) + if err != nil { + return nil + } + kafkaClusterId := kafkaClusterConfig.GetId() + rtceTopics, err := c.V2Client.ListRtceTopics("", "", environmentId, kafkaClusterId) + if err != nil { + return nil + } + + suggestions := make([]string, len(rtceTopics)) + for i, rtceTopic := range rtceTopics { + suggestions[i] = rtceTopic.Spec.GetTopicName() + } + return suggestions +} diff --git a/internal/rtce/command_rtce_topic_create.go b/internal/rtce/command_rtce_topic_create.go new file mode 100644 index 0000000000..ae9a59b90f --- /dev/null +++ b/internal/rtce/command_rtce_topic_create.go @@ -0,0 +1,88 @@ +package rtce + +import ( + "strings" + + "github.com/spf13/cobra" + + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/errors" + "github.com/confluentinc/cli/v4/pkg/kafka" +) + +func (c *rtceTopicCommand) newCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a RTCE topic.", + Args: cobra.NoArgs, + RunE: c.create, + } + pcmd.AddCloudAwsFlag(cmd) + cmd.Flags().String("description", "", "A model-readable description of the RTCE topic.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddClusterFlag(cmd, c.AuthenticatedCLICommand) + cmd.Flags().String("region", "", "The cloud region where the RTCE topic is deployed.") + cmd.Flags().String("topic-name", "", "The Kafka topic name containing the data for the RTCE topic.") + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + cobra.CheckErr(cmd.MarkFlagRequired("cloud")) + cobra.CheckErr(cmd.MarkFlagRequired("description")) + cobra.CheckErr(cmd.MarkFlagRequired("region")) + cobra.CheckErr(cmd.MarkFlagRequired("topic-name")) + + return cmd +} + +func (c *rtceTopicCommand) create(cmd *cobra.Command, args []string) error { + spec := rtcev1.RtceV1RtceTopicSpec{} + createReq := rtcev1.RtceV1RtceTopic{} + + cloud, err := cmd.Flags().GetString("cloud") + if err != nil { + return err + } + cloud = strings.ToUpper(cloud) + spec.Cloud = rtcev1.PtrString(cloud) + + description, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + spec.Description = rtcev1.PtrString(description) + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + spec.Environment = &rtcev1.EnvScopedObjectReference{Id: environmentId} + + kafkaClusterConfig, err := kafka.GetClusterForCommand(c.V2Client, c.Context) + if err != nil { + return err + } + kafkaClusterId := kafkaClusterConfig.GetId() + spec.KafkaCluster = &rtcev1.EnvScopedObjectReference{Id: kafkaClusterId} + + region, err := cmd.Flags().GetString("region") + if err != nil { + return err + } + spec.Region = rtcev1.PtrString(region) + + topicName, err := cmd.Flags().GetString("topic-name") + if err != nil { + return err + } + spec.TopicName = rtcev1.PtrString(topicName) + + createReq.Spec = &spec + rtceTopic, httpResp, err := c.V2Client.CreateRtceTopic(createReq) + if err != nil { + return errors.CatchCCloudV2Error(err, httpResp) + } + + return printRtceTopic(cmd, rtceTopic) +} diff --git a/internal/rtce/command_rtce_topic_delete.go b/internal/rtce/command_rtce_topic_delete.go new file mode 100644 index 0000000000..8aa2936b21 --- /dev/null +++ b/internal/rtce/command_rtce_topic_delete.go @@ -0,0 +1,67 @@ +package rtce + +import ( + "fmt" + + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/deletion" + "github.com/confluentinc/cli/v4/pkg/kafka" + "github.com/confluentinc/cli/v4/pkg/output" + "github.com/confluentinc/cli/v4/pkg/plural" + "github.com/confluentinc/cli/v4/pkg/utils" +) + +func (c *rtceTopicCommand) newDeleteCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [topic-name-2] ... [topic-name-n]", + Short: "Delete one or more RTCE topics.", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validArgsMultiple), + RunE: c.delete, + } + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddClusterFlag(cmd, c.AuthenticatedCLICommand) + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddForceFlag(cmd) + + return cmd +} + +func (c *rtceTopicCommand) delete(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + kafkaClusterConfig, err := kafka.GetClusterForCommand(c.V2Client, c.Context) + if err != nil { + return err + } + kafkaClusterId := kafkaClusterConfig.GetId() + + existenceFunc := func(primaryId string) bool { + _, _, err := c.V2Client.GetRtceTopic(primaryId, environmentId, kafkaClusterId) + return err == nil + } + + if err := deletion.ValidateAndConfirm(cmd, args, existenceFunc, "RTCE topic"); err != nil { + return err + } + + deleteFunc := func(primaryId string) error { + return c.V2Client.DeleteRtceTopic(primaryId, environmentId, kafkaClusterId) + } + + deletedIds, err := deletion.DeleteWithoutMessage(cmd, args, deleteFunc) + deleteMsg := "Requested to delete %s %s.\n" + if len(deletedIds) == 1 { + output.Printf(c.Config.EnableColor, deleteMsg, "RTCE topic", fmt.Sprintf(`"%s"`, deletedIds[0])) + } else if len(deletedIds) > 1 { + output.Printf(c.Config.EnableColor, deleteMsg, plural.Plural("RTCE topic"), utils.ArrayToCommaDelimitedString(deletedIds, "and")) + } + + return err +} diff --git a/internal/rtce/command_rtce_topic_describe.go b/internal/rtce/command_rtce_topic_describe.go new file mode 100644 index 0000000000..210e69c730 --- /dev/null +++ b/internal/rtce/command_rtce_topic_describe.go @@ -0,0 +1,47 @@ +package rtce + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/errors" + "github.com/confluentinc/cli/v4/pkg/kafka" +) + +func (c *rtceTopicCommand) newDescribeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "describe ", + Short: "Describe a RTCE topic.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validArgs), + RunE: c.describe, + } + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddClusterFlag(cmd, c.AuthenticatedCLICommand) + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *rtceTopicCommand) describe(cmd *cobra.Command, args []string) error { + topicName := args[0] + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + kafkaClusterConfig, err := kafka.GetClusterForCommand(c.V2Client, c.Context) + if err != nil { + return err + } + kafkaClusterId := kafkaClusterConfig.GetId() + + rtceTopic, httpResp, err := c.V2Client.GetRtceTopic(topicName, environmentId, kafkaClusterId) + if err != nil { + return errors.CatchCCloudV2Error(err, httpResp) + } + + return printRtceTopic(cmd, rtceTopic) +} diff --git a/internal/rtce/command_rtce_topic_list.go b/internal/rtce/command_rtce_topic_list.go new file mode 100644 index 0000000000..2b26254be6 --- /dev/null +++ b/internal/rtce/command_rtce_topic_list.go @@ -0,0 +1,74 @@ +package rtce + +import ( + "strings" + + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/kafka" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *rtceTopicCommand) newListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List RTCE topics.", + Args: cobra.NoArgs, + RunE: c.list, + } + pcmd.AddCloudAwsFlag(cmd) + cmd.Flags().String("region", "", "Filter the results by exact match for spec.region.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddClusterFlag(cmd, c.AuthenticatedCLICommand) + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *rtceTopicCommand) list(cmd *cobra.Command, _ []string) error { + cloud, err := cmd.Flags().GetString("cloud") + if err != nil { + return err + } + cloud = strings.ToUpper(cloud) + + region, err := cmd.Flags().GetString("region") + if err != nil { + return err + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + kafkaClusterConfig, err := kafka.GetClusterForCommand(c.V2Client, c.Context) + if err != nil { + return err + } + kafkaClusterId := kafkaClusterConfig.GetId() + + rtceTopics, err := c.V2Client.ListRtceTopics(cloud, region, environmentId, kafkaClusterId) + if err != nil { + return err + } + + list := output.NewList(cmd) + for _, rtceTopic := range rtceTopics { + out := &rtceTopicOut{ + TopicName: rtceTopic.Spec.GetTopicName(), + Cloud: rtceTopic.Spec.GetCloud(), + Description: rtceTopic.Spec.GetDescription(), + Environment: rtceTopic.Spec.Environment.GetId(), + KafkaCluster: rtceTopic.Spec.KafkaCluster.GetId(), + Region: rtceTopic.Spec.GetRegion(), + ErrorMessage: rtceTopic.Status.GetErrorMessage(), + Phase: rtceTopic.Status.GetPhase(), + } + list.Add(out) + } + return list.Print() +} diff --git a/internal/rtce/command_rtce_topic_update.go b/internal/rtce/command_rtce_topic_update.go new file mode 100644 index 0000000000..19530f5373 --- /dev/null +++ b/internal/rtce/command_rtce_topic_update.go @@ -0,0 +1,73 @@ +package rtce + +import ( + "github.com/spf13/cobra" + + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/errors" + "github.com/confluentinc/cli/v4/pkg/kafka" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *rtceTopicCommand) newUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a RTCE topic.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validArgs), + RunE: c.update, + } + + // Optional: flags for updatable attributes only + cmd.Flags().String("description", "", "A model-readable description of the RTCE topic.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddClusterFlag(cmd, c.AuthenticatedCLICommand) + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *rtceTopicCommand) update(cmd *cobra.Command, args []string) error { + topicName := args[0] + + updateReq := rtcev1.RtceV1RtceTopicUpdate{} + specUpdate := rtcev1.RtceV1RtceTopicSpecUpdate{} + + description, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + if description != "" { + specUpdate.Description = rtcev1.PtrString(description) + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + if environmentId != "" { + specUpdate.Environment = &rtcev1.EnvScopedObjectReference{Id: environmentId} + } + + kafkaClusterConfig, err := kafka.GetClusterForCommand(c.V2Client, c.Context) + if err != nil { + return err + } + kafkaClusterId := kafkaClusterConfig.GetId() + if kafkaClusterId != "" { + specUpdate.KafkaCluster = &rtcev1.EnvScopedObjectReference{Id: kafkaClusterId} + } + + updateReq.Spec = &specUpdate + rtceTopic, httpResp, err := c.V2Client.UpdateRtceTopic(topicName, updateReq) + if err != nil { + return errors.CatchCCloudV2Error(err, httpResp) + } + + output.Printf(c.Config.EnableColor, "Updated RTCE topic \"%s\".\n", topicName) + return printRtceTopic(cmd, rtceTopic) +} diff --git a/pkg/ccloudv2/client.go b/pkg/ccloudv2/client.go index 6f5f1d47b9..830595b458 100644 --- a/pkg/ccloudv2/client.go +++ b/pkg/ccloudv2/client.go @@ -31,6 +31,7 @@ import ( orgv2 "github.com/confluentinc/ccloud-sdk-go-v2/org/v2" pi "github.com/confluentinc/ccloud-sdk-go-v2/provider-integration/v1" piv2 "github.com/confluentinc/ccloud-sdk-go-v2/provider-integration/v2" + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" servicequotav1 "github.com/confluentinc/ccloud-sdk-go-v2/service-quota/v1" srcmv3 "github.com/confluentinc/ccloud-sdk-go-v2/srcm/v3" ssov2 "github.com/confluentinc/ccloud-sdk-go-v2/sso/v2" @@ -75,6 +76,7 @@ type Client struct { OrgClient *orgv2.APIClient ProviderIntegrationClient *pi.APIClient ProviderIntegrationV2Client *piv2.APIClient + RtceClient *rtcev1.APIClient ServiceQuotaClient *servicequotav1.APIClient SrcmClient *srcmv3.APIClient SsoClient *ssov2.APIClient @@ -126,6 +128,7 @@ func NewClient(cfg *config.Config, unsafeTrace bool) *Client { OrgClient: newOrgClient(httpClient, url, userAgent, unsafeTrace), ProviderIntegrationClient: newProviderIntegrationClient(httpClient, url, userAgent, unsafeTrace), ProviderIntegrationV2Client: newProviderIntegrationV2Client(httpClient, url, userAgent, unsafeTrace), + RtceClient: newRtceClient(httpClient, url, userAgent, unsafeTrace), ServiceQuotaClient: newServiceQuotaClient(httpClient, url, userAgent, unsafeTrace), SrcmClient: newSrcmClient(httpClient, url, userAgent, unsafeTrace), SsoClient: newSsoClient(httpClient, url, userAgent, unsafeTrace), diff --git a/pkg/ccloudv2/rtce.go b/pkg/ccloudv2/rtce.go new file mode 100644 index 0000000000..fb7a5c2c86 --- /dev/null +++ b/pkg/ccloudv2/rtce.go @@ -0,0 +1,137 @@ +package ccloudv2 + +import ( + "context" + "net/http" + + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" + + "github.com/confluentinc/cli/v4/pkg/errors" +) + +// ===== API group client bootstrap ===== + +func newRtceClient(httpClient *http.Client, url, userAgent string, unsafeTrace bool) *rtcev1.APIClient { + cfg := rtcev1.NewConfiguration() + cfg.Debug = unsafeTrace + cfg.HTTPClient = httpClient + cfg.Servers = rtcev1.ServerConfigurations{{URL: url}} + cfg.UserAgent = userAgent + + return rtcev1.NewAPIClient(cfg) +} + +func (c *Client) rtceApiContext() context.Context { + return context.WithValue(context.Background(), rtcev1.ContextAccessToken, c.cfg.Context().GetAuthToken()) +} + +// ===== RTCE topics API calls ===== + +func (c *Client) CreateRtceTopic(req rtcev1.RtceV1RtceTopic) (rtcev1.RtceV1RtceTopic, *http.Response, error) { + createReq := c.RtceClient.RtceTopicsRtceV1Api. + CreateRtceV1RtceTopic(c.rtceApiContext()). + RtceV1RtceTopic(req) + return createReq.Execute() +} + +func (c *Client) GetRtceTopic(topicName string, environment string, specKafkaCluster string) (rtcev1.RtceV1RtceTopic, *http.Response, error) { + getReq := c.RtceClient.RtceTopicsRtceV1Api. + GetRtceV1RtceTopic(c.rtceApiContext(), topicName) + getReq = getReq.Environment(environment) + getReq = getReq.SpecKafkaCluster(specKafkaCluster) + return getReq.Execute() +} + +func (c *Client) UpdateRtceTopic(topicName string, update rtcev1.RtceV1RtceTopicUpdate) (rtcev1.RtceV1RtceTopic, *http.Response, error) { + updateReq := c.RtceClient.RtceTopicsRtceV1Api. + UpdateRtceV1RtceTopic(c.rtceApiContext(), topicName). + RtceV1RtceTopicUpdate(update) + return updateReq.Execute() +} + +func (c *Client) DeleteRtceTopic(topicName string, environment string, specKafkaCluster string) error { + deleteReq := c.RtceClient.RtceTopicsRtceV1Api. + DeleteRtceV1RtceTopic(c.rtceApiContext(), topicName) + deleteReq = deleteReq.Environment(environment) + deleteReq = deleteReq.SpecKafkaCluster(specKafkaCluster) + httpResp, err := deleteReq.Execute() + return errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) ListRtceTopics(specCloud string, specRegion string, environment string, specKafkaCluster string) ([]rtcev1.RtceV1RtceTopic, error) { + var list []rtcev1.RtceV1RtceTopic + + done := false + pageToken := "" + for !done { + page, httpResp, err := c.executeListRtceTopics(specCloud, specRegion, environment, specKafkaCluster, pageToken) + if err != nil { + return nil, errors.CatchCCloudV2Error(err, httpResp) + } + list = append(list, page.GetData()...) + + pageToken, done, err = extractNextPageToken(page.GetMetadata().Next) + if err != nil { + return nil, err + } + } + + return list, nil +} + +func (c *Client) executeListRtceTopics(specCloud string, specRegion string, environment string, specKafkaCluster string, pageToken string) (rtcev1.RtceV1RtceTopicList, *http.Response, error) { + req := c.RtceClient.RtceTopicsRtceV1Api. + ListRtceV1RtceTopics(c.rtceApiContext()). + Environment(environment). + SpecKafkaCluster(specKafkaCluster). + PageSize(ccloudV2ListPageSize) + if specCloud != "" { + req = req.SpecCloud(specCloud) + } + if specRegion != "" { + req = req.SpecRegion(specRegion) + } + if pageToken != "" { + req = req.PageToken(pageToken) + } + return req.Execute() +} + +// ===== RTCE regions API calls ===== + +func (c *Client) ListRtceRegions(cloud string, region string) ([]rtcev1.RtceV1Region, error) { + var list []rtcev1.RtceV1Region + + done := false + pageToken := "" + for !done { + page, httpResp, err := c.executeListRegions(cloud, region, pageToken) + if err != nil { + return nil, errors.CatchCCloudV2Error(err, httpResp) + } + list = append(list, page.GetData()...) + + pageToken, done, err = extractNextPageToken(page.GetMetadata().Next) + if err != nil { + return nil, err + } + } + + return list, nil +} + +func (c *Client) executeListRegions(cloud string, region string, pageToken string) (rtcev1.RtceV1RegionList, *http.Response, error) { + req := c.RtceClient.RegionsRtceV1Api. + ListRtceV1Regions(c.rtceApiContext()). + PageSize(ccloudV2ListPageSize) + if cloud != "" { + req = req.Cloud(cloud) + } + if region != "" { + req = req.Region(region) + } + if pageToken != "" { + req = req.PageToken(pageToken) + } + return req.Execute() +} diff --git a/test/fixtures/input/rtce/region/create_region.json b/test/fixtures/input/rtce/region/create_region.json new file mode 100644 index 0000000000..8fd18c727c --- /dev/null +++ b/test/fixtures/input/rtce/region/create_region.json @@ -0,0 +1,15 @@ +{ + "api_version": "rtce/v1", + "cloud": "AWS", + "display_name": "US East (Ohio)", + "id": "dlz-f3a90de", + "kind": "Region", + "metadata": { + "created_at": "2006-01-02T15:04:05-07:00", + "deleted_at": "2006-01-02T15:04:05-07:00", + "resource_name": "crn://confluent.cloud/organization=9bb441c4-edef-46ac-8a41-c49e44a3fd9a/region=r-12345", + "self": "https://api.confluent.cloud/rtce/v1/regions/r-12345", + "updated_at": "2006-01-02T15:04:05-07:00" + }, + "region": "us-east-2" +} diff --git a/test/fixtures/input/rtce/region/read_created_region.json b/test/fixtures/input/rtce/region/read_created_region.json new file mode 100644 index 0000000000..8fd18c727c --- /dev/null +++ b/test/fixtures/input/rtce/region/read_created_region.json @@ -0,0 +1,15 @@ +{ + "api_version": "rtce/v1", + "cloud": "AWS", + "display_name": "US East (Ohio)", + "id": "dlz-f3a90de", + "kind": "Region", + "metadata": { + "created_at": "2006-01-02T15:04:05-07:00", + "deleted_at": "2006-01-02T15:04:05-07:00", + "resource_name": "crn://confluent.cloud/organization=9bb441c4-edef-46ac-8a41-c49e44a3fd9a/region=r-12345", + "self": "https://api.confluent.cloud/rtce/v1/regions/r-12345", + "updated_at": "2006-01-02T15:04:05-07:00" + }, + "region": "us-east-2" +} diff --git a/test/fixtures/input/rtce/rtce_topic/create_rtce_topic.json b/test/fixtures/input/rtce/rtce_topic/create_rtce_topic.json new file mode 100644 index 0000000000..4b00c93759 --- /dev/null +++ b/test/fixtures/input/rtce/rtce_topic/create_rtce_topic.json @@ -0,0 +1,33 @@ +{ + "api_version": "rtce/v1", + "kind": "RtceTopic", + "metadata": { + "created_at": "2006-01-02T15:04:05-07:00", + "deleted_at": "2006-01-02T15:04:05-07:00", + "resource_name": "crn://confluent.cloud/organization=9bb441c4-edef-46ac-8a41-c49e44a3fd9a/environment=env-abc123/cloud-cluster=lkc-12345/topic=tt-12345", + "self": "https://api.confluent.cloud/rtce/v1/rtce-topics/rt-12345", + "updated_at": "2006-01-02T15:04:05-07:00" + }, + "spec": { + "cloud": "AWS", + "description": "Customer orders table for real-time analytics", + "environment": { + "environment": "", + "id": "", + "related": "https://example.com", + "resource_name": "https://example.com" + }, + "kafka_cluster": { + "environment": "", + "id": "", + "related": "https://example.com", + "resource_name": "https://example.com" + }, + "region": "us-west-2", + "topic_name": "orders_topic" + }, + "status": { + "error_message": "Failed to provision table: insufficient resources", + "phase": "PENDING" + } +} diff --git a/test/fixtures/input/rtce/rtce_topic/read_created_rtce_topic.json b/test/fixtures/input/rtce/rtce_topic/read_created_rtce_topic.json new file mode 100644 index 0000000000..1de5461a55 --- /dev/null +++ b/test/fixtures/input/rtce/rtce_topic/read_created_rtce_topic.json @@ -0,0 +1,33 @@ +{ + "api_version": "rtce/v1", + "kind": "RtceTopic", + "metadata": { + "created_at": "2006-01-02T15:04:05-07:00", + "deleted_at": "2006-01-02T15:04:05-07:00", + "resource_name": "crn://confluent.cloud/organization=9bb441c4-edef-46ac-8a41-c49e44a3fd9a/environment=env-abc123/cloud-cluster=lkc-12345/topic=tt-12345", + "self": "https://api.confluent.cloud/rtce/v1/rtce-topics/rt-12345", + "updated_at": "2006-01-02T15:04:05-07:00" + }, + "spec": { + "cloud": "AWS", + "description": "Customer orders table for real-time analytics", + "environment": { + "environment": "", + "id": "", + "related": "https://example.com", + "resource_name": "https://example.com" + }, + "kafka_cluster": { + "environment": "", + "id": "", + "related": "https://example.com", + "resource_name": "https://example.com" + }, + "region": "us-west-2", + "topic_name": "orders_topic" + }, + "status": { + "error_message": "Failed to provision table: insufficient resources", + "phase": "ACTIVE" + } +} diff --git a/test/live/region_live_test.go b/test/live/region_live_test.go new file mode 100644 index 0000000000..bae3ca7b29 --- /dev/null +++ b/test/live/region_live_test.go @@ -0,0 +1,34 @@ +//go:build live_test && (all || rtce) + +package live + +import ( + "testing" +) + +func (s *CLILiveTestSuite) TestRegionCRUDLive() { + t := s.T() + t.Parallel() + state := s.setupTestContext(t) + + // Variables + regionName := uniqueName("region") + + // Cleanup (LIFO) + + steps := []CLILiveTest{ + { + Name: "List RTCE regions", + Args: "rtce region list", + UseStateVars: true, + ExitCode: 0, + Contains: []string{regionName}, + }, + } + + for _, step := range steps { + t.Run(step.Name, func(t *testing.T) { + s.runLiveCommand(t, step, state) + }) + } +} diff --git a/test/live/rtce_topic_live_test.go b/test/live/rtce_topic_live_test.go new file mode 100644 index 0000000000..a0738ef31d --- /dev/null +++ b/test/live/rtce_topic_live_test.go @@ -0,0 +1,113 @@ +//go:build live_test && (all || rtce) + +package live + +import ( + "strings" + "testing" + "time" +) + +func (s *CLILiveTestSuite) TestRtceTopicCRUDLive() { + t := s.T() + t.Parallel() + state := s.setupTestContext(t) + + // Variables + rtceTopicName := uniqueName("rtceto") + description := "Live test description" + updatedDescription := "Updated live test description" + + // Cleanup (LIFO) + s.registerCleanup(t, "environment delete {{.env_id}} --force", state) + s.registerCleanup(t, "rtce rtce-topic delete {{.rtce_topic_id}} --environment {{.env_id}} --force", state) + + // Phase 1: Create steps + createSteps := []CLILiveTest{ + { + Name: "Create environment", + Args: "environment create " + uniqueName("env") + " -o json", + JSONFieldsExist: []string{"id"}, + CaptureID: "env_id", + }, + { + Name: "Use environment", + Args: "environment use {{.env_id}}", + UseStateVars: true, + }, + { + Name: "Create RTCE topic", + Args: "rtce rtce-topic create " + rtceTopicName + " --description \"" + description + "\" --region \"" + region + "\" --topic-name \"" + topicName + "\" --environment {{.env_id}} -o json", + UseStateVars: true, + CaptureID: "rtce_topic_id", + JSONFields: map[string]string{}, + JSONFieldsExist: []string{"id"}, + }, + } + + for _, step := range createSteps { + t.Run(step.Name, func(t *testing.T) { + s.runLiveCommand(t, step, state) + }) + } + + // Phase 2: Wait for provisioning + t.Run("Wait for RTCE topic provisioned", func(t *testing.T) { + s.waitForCondition(t, + "rtce rtce-topic describe {{.rtce_topic_id}} --environment {{.env_id}} -o json", + state, + func(output string) bool { + status := extractJSONField(t, output, "phase") + return strings.EqualFold(status, "ACTIVE") + }, + 30*time.Second, + 10*time.Minute, + ) + }) + + // Phase 3: CRUD operations + crudSteps := []CLILiveTest{ + { + Name: "Describe RTCE topic", + Args: "rtce rtce-topic describe {{.rtce_topic_id}} --environment {{.env_id}} -o json", + UseStateVars: true, + JSONFields: map[string]string{}, + }, + { + Name: "List RTCE topics", + Args: "rtce rtce-topic list --environment {{.env_id}}", + UseStateVars: true, + Contains: []string{rtceTopicName}, + }, + { + Name: "Update RTCE topic description", + Args: "rtce rtce-topic update {{.rtce_topic_id}} --description \"" + updatedDescription + "\" --environment {{.env_id}}", + UseStateVars: true, + }, + { + Name: "Describe updated RTCE topic", + Args: "rtce rtce-topic describe {{.rtce_topic_id}} --environment {{.env_id}} -o json", + UseStateVars: true, + JSONFields: map[string]string{ + "description": updatedDescription, + }, + }, + { + Name: "Delete RTCE topic", + Args: "rtce rtce-topic delete {{.rtce_topic_id}} --environment {{.env_id}} --force", + UseStateVars: true, + }, + { + Name: "Verify deletion", + Args: "rtce rtce-topic describe {{.rtce_topic_id}} --environment {{.env_id}}", + UseStateVars: true, + ExitCode: 1, + }, + } + + for _, step := range crudSteps { + t.Run(step.Name, func(t *testing.T) { + s.runLiveCommand(t, step, state) + }) + } +} diff --git a/test/region_test.go b/test/region_test.go new file mode 100644 index 0000000000..eec4e2ad5c --- /dev/null +++ b/test/region_test.go @@ -0,0 +1,15 @@ +package test + +func (s *CLITestSuite) TestRtceRegionList() { + tests := []CLITest{ + {args: "rtce region list", fixture: "rtce/region/list.golden"}, + {args: "rtce region list --region us-east-2", fixture: "rtce/region/list-region.golden"}, + {args: "rtce region list -o json", fixture: "rtce/region/list-json.golden"}, + {args: "rtce region list -o yaml", fixture: "rtce/region/list-yaml.golden"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} diff --git a/test/rtce_topic_test.go b/test/rtce_topic_test.go new file mode 100644 index 0000000000..745feb25bf --- /dev/null +++ b/test/rtce_topic_test.go @@ -0,0 +1,78 @@ +package test + +func (s *CLITestSuite) TestRtceRtceTopicCreate() { + tests := []CLITest{ + {args: "rtce rtce-topic create --cloud aws --description \"Customer orders table for real-time analytics\" --region us-west-2 --topic-name orders_topic", fixture: "rtce/rtce-topic/create.golden", useKafka: "lkc-abc123"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} + +func (s *CLITestSuite) TestRtceRtceTopicDelete() { + tests := []CLITest{ + {args: "rtce rtce-topic delete id-1 --force", fixture: "rtce/rtce-topic/delete.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic delete id-1", input: "y\n", fixture: "rtce/rtce-topic/delete-no-force.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic delete id-1 id-2", input: "y\n", fixture: "rtce/rtce-topic/delete-multiple.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic delete invalid", fixture: "rtce/rtce-topic/delete-invalid.golden", exitCode: 1, useKafka: "lkc-abc123"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} + +func (s *CLITestSuite) TestRtceRtceTopicDescribe() { + tests := []CLITest{ + {args: "rtce rtce-topic describe id-1", fixture: "rtce/rtce-topic/describe.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic describe id-1 -o json", fixture: "rtce/rtce-topic/describe-json.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic describe id-1 -o yaml", fixture: "rtce/rtce-topic/describe-yaml.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic describe invalid", fixture: "rtce/rtce-topic/describe-invalid.golden", exitCode: 1, useKafka: "lkc-abc123"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} + +func (s *CLITestSuite) TestRtceRtceTopicList() { + tests := []CLITest{ + {args: "rtce rtce-topic list", fixture: "rtce/rtce-topic/list.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic list --region us-west-2", fixture: "rtce/rtce-topic/list-region.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic list -o json", fixture: "rtce/rtce-topic/list-json.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic list -o yaml", fixture: "rtce/rtce-topic/list-yaml.golden", useKafka: "lkc-abc123"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} + +func (s *CLITestSuite) TestRtceRtceTopicUpdate() { + tests := []CLITest{ + {args: "rtce rtce-topic update id-1 --description \"Customer orders table for real-time analytics\"", fixture: "rtce/rtce-topic/update-description.golden", useKafka: "lkc-abc123"}, + {args: "rtce rtce-topic update invalid", fixture: "rtce/rtce-topic/update-invalid.golden", exitCode: 1, useKafka: "lkc-abc123"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} + +func (s *CLITestSuite) TestRtceRtceTopic_Autocomplete() { + tests := []CLITest{ + {args: "__complete rtce rtce-topic delete \"\"", fixture: "rtce/rtce-topic/delete-autocomplete.golden"}, + {args: "__complete rtce rtce-topic describe \"\"", fixture: "rtce/rtce-topic/describe-autocomplete.golden"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} diff --git a/test/test-server/ccloudv2_router.go b/test/test-server/ccloudv2_router.go index d942681e18..220786940a 100644 --- a/test/test-server/ccloudv2_router.go +++ b/test/test-server/ccloudv2_router.go @@ -122,6 +122,9 @@ var ccloudV2Routes = []route{ {"/pim/v2/integrations", handleProviderIntegrationsV2}, {"/pim/v2/integrations/{id}", handleProviderIntegrationV2}, {"/pim/v2/integrations/validate", handleProviderIntegrationV2Validate}, + {"/rtce/v1/regions", handleRtceV1Regions}, + {"/rtce/v1/rtce-topics", handleRtceV1RtceTopics}, + {"/rtce/v1/rtce-topics/{topic_name}", handleRtceV1RtceTopicsTopicName}, {"/service-quota/v1/applied-quotas", handleAppliedQuotas}, {"/service-quota/v2/applied-quotas", handleAppliedQuotas}, {"/srcm/v3/clusters", handleSchemaRegistryClustersV3}, diff --git a/test/test-server/region_handler.go b/test/test-server/region_handler.go new file mode 100644 index 0000000000..2f5d929603 --- /dev/null +++ b/test/test-server/region_handler.go @@ -0,0 +1,42 @@ +package testserver + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" +) + +// Handler for "/rtce/v1/regions" +func handleRtceV1Regions(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + region := readRtceV1RegionFile(t, "read_created_region.json") + + regionList := &rtcev1.RtceV1RegionList{ + Data: []rtcev1.RtceV1Region{region}, + } + + err := json.NewEncoder(w).Encode(regionList) + require.NoError(t, err) + } + } +} + +func readRtceV1RegionFile(t *testing.T, filename string) rtcev1.RtceV1Region { + jsonPath := filepath.Join("test", "fixtures", "input", "rtce", "region", filename) + jsonData, err := os.ReadFile(jsonPath) + require.NoError(t, err) + + region := rtcev1.RtceV1Region{} + err = json.Unmarshal(jsonData, ®ion) + require.NoError(t, err) + + return region +} diff --git a/test/test-server/rtce_topic_handler.go b/test/test-server/rtce_topic_handler.go new file mode 100644 index 0000000000..c62bc00033 --- /dev/null +++ b/test/test-server/rtce_topic_handler.go @@ -0,0 +1,92 @@ +package testserver + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" + + rtcev1 "github.com/confluentinc/ccloud-sdk-go-v2/rtce/v1" +) + +// Handler for "/rtce/v1/rtce-topics" +func handleRtceV1RtceTopics(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + rtceTopic := readRtceV1RtceTopicFile(t, "read_created_rtce_topic.json") + + rtceTopicList := &rtcev1.RtceV1RtceTopicList{ + Data: []rtcev1.RtceV1RtceTopic{rtceTopic}, + } + + err := json.NewEncoder(w).Encode(rtceTopicList) + require.NoError(t, err) + case http.MethodPost: + rtceTopic := readRtceV1RtceTopicFile(t, "create_rtce_topic.json") + + // Overwrite updated fields using the request body + err := json.NewDecoder(r.Body).Decode(&rtceTopic) + require.NoError(t, err) + + err = json.NewEncoder(w).Encode(rtceTopic) + require.NoError(t, err) + } + } +} + +// Handler for "/rtce/v1/rtce-topics/{topic_name}" +func handleRtceV1RtceTopicsTopicName(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["topic_name"] + switch r.Method { + case http.MethodGet: + switch topic_name { + case "invalid": + w.WriteHeader(http.StatusNotFound) + default: + rtceTopic := readRtceV1RtceTopicFile(t, "read_created_rtce_topic.json") + + err := json.NewEncoder(w).Encode(&rtceTopic) + require.NoError(t, err) + } + case http.MethodPatch: + switch topic_name { + case "invalid": + w.WriteHeader(http.StatusNotFound) + default: + rtceTopic := readRtceV1RtceTopicFile(t, "read_created_rtce_topic.json") + + // Overwrite updated fields using the request body + err := json.NewDecoder(r.Body).Decode(&rtceTopic) + require.NoError(t, err) + + err = json.NewEncoder(w).Encode(rtceTopic) + require.NoError(t, err) + } + case http.MethodDelete: + switch topic_name { + case "invalid": + w.WriteHeader(http.StatusNotFound) + default: + w.WriteHeader(http.StatusNoContent) + } + } + } +} + +func readRtceV1RtceTopicFile(t *testing.T, filename string) rtcev1.RtceV1RtceTopic { + jsonPath := filepath.Join("test", "fixtures", "input", "rtce", "rtce_topic", filename) + jsonData, err := os.ReadFile(jsonPath) + require.NoError(t, err) + + rtceTopic := rtcev1.RtceV1RtceTopic{} + err = json.Unmarshal(jsonData, &rtceTopic) + require.NoError(t, err) + + return rtceTopic +}