Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
# Load .env file if it exists (for local development)
# In CI/CD, environment variables are provided by the environment
-include .env
ifneq (,$(wildcard .env))
export $(shell sed 's/=.*//' .env)
endif
export

all: gen build test

Expand Down
6 changes: 3 additions & 3 deletions temporalcloudcli/commands.namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error
}

// Poll for completion
return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
}

func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error {
Expand Down Expand Up @@ -182,7 +182,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error
}

// Step 7: Poll for completion
return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
}

func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) error {
Expand Down Expand Up @@ -234,7 +234,7 @@ func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) erro
}

// Poll for completion
return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace)
return PollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace)
}

func (c *CloudNamespaceListCommand) run(cctx *CommandContext, _ []string) error {
Expand Down
2 changes: 1 addition & 1 deletion temporalcloudcli/commands.namespace.lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,5 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string
}

// Poll for completion
return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
}
2 changes: 1 addition & 1 deletion temporalcloudcli/commands.namespace.retention.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string
}

// Poll for completion
return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
return PollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace)
}

func (c *CloudNamespaceRetentionGetCommand) run(cctx *CommandContext, _ []string) error {
Expand Down
6 changes: 6 additions & 0 deletions temporalcloudcli/commands.namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"go.temporal.io/api/temporalproto"
"go.temporal.io/cloud-sdk/api/cloudservice/v1"
namespace "go.temporal.io/cloud-sdk/api/namespace/v1"
resource "go.temporal.io/cloud-sdk/api/resource/v1"
"google.golang.org/protobuf/encoding/protojson"
)

Expand Down Expand Up @@ -215,6 +216,11 @@ func (s *SharedServerSuite) cleanupNamespaces() {
pageToken = res.NextPageToken
}
for _, ns := range namespacesToClean {
// AIDEV-NOTE: Only delete namespaces that are in ACTIVE state (value 3).
// Namespaces in other states (DELETING, DELETED, etc.) cannot be deleted.
if ns.State != resource.ResourceState_RESOURCE_STATE_ACTIVE {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if its better for the test to fail when a namespace is left in non-active state. I would hate to leak namespaces in the test account.
Ideally the test should clean up all the namespaces it creates, else its a bug that needs fixing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout, I'll address this in a follow up PR

continue
}
res, err := cloudClient.CloudService().DeleteNamespace(s.Context, &cloudservice.DeleteNamespaceRequest{
ResourceVersion: ns.ResourceVersion,
Namespace: ns.Namespace,
Expand Down
53 changes: 10 additions & 43 deletions temporalcloudcli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"os"
"regexp"
Expand All @@ -17,8 +18,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/temporalio/cloud-cli/temporalcloudcli"
"go.temporal.io/cloud-sdk/api/cloudservice/v1"
operation "go.temporal.io/cloud-sdk/api/operation/v1"
"github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer"
"go.temporal.io/cloud-sdk/cloudclient"
)

Expand Down Expand Up @@ -225,51 +225,18 @@ func (s *SharedServerSuite) getCloudClient() *cloudclient.Client {
}

// pollAsyncOperation polls an async operation until it reaches a terminal state.
// It prints status updates every second and returns the final AsyncOperation.
// This is a test wrapper around the PollAsyncOperation function.
func (s *SharedServerSuite) pollAsyncOperation(
cloudClient *cloudclient.Client,
operationID string,
) error {

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for {
select {
case <-s.Context.Done():
return fmt.Errorf("operation polling cancelled: %w", s.Context.Err())
case <-ticker.C:
// Get the current state of the operation
resp, err := cloudClient.CloudService().GetAsyncOperation(s.Context, &cloudservice.GetAsyncOperationRequest{
AsyncOperationId: operationID,
})
if err != nil {
return fmt.Errorf("failed to get async operation status: %w", err)
}

asyncOp := resp.GetAsyncOperation()
if asyncOp == nil {
return fmt.Errorf("async operation not found")
}

// Print current state
switch asyncOp.State {
case operation.AsyncOperation_STATE_PENDING, operation.AsyncOperation_STATE_IN_PROGRESS:
case operation.AsyncOperation_STATE_FULFILLED:
return nil
case operation.AsyncOperation_STATE_FAILED:
return fmt.Errorf("async operation failed: %s", asyncOp.FailureReason)
case operation.AsyncOperation_STATE_CANCELLED:
return fmt.Errorf("async operation cancelled")
case operation.AsyncOperation_STATE_REJECTED:
return fmt.Errorf("async operation rejected")
default:
}
}
// Create a minimal CommandContext for testing (with discard printer to skip output)
cctx := &temporalcloudcli.CommandContext{
Context: s.Context,
Printer: &printer.Printer{
Output: io.Discard, // Discard all output for tests
},
}
}

type mutationResult struct {
asyncOp *operation.AsyncOperation
ID string
return temporalcloudcli.PollAsyncOperation(cctx, cloudClient, operationID, "")
}
26 changes: 19 additions & 7 deletions temporalcloudcli/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func promptApplyResource(cctx *CommandContext, existing, actual proto.Message, v
//
// AIDEV-NOTE: This function takes a pre-built cloudClient. Commands should
// build the client using cctx.BuildCloudClient() and pass it directly.
func pollAsyncOperation(
func PollAsyncOperation(
cctx *CommandContext,
cloudClient *cloudclient.Client,
operationID string,
Expand Down Expand Up @@ -192,22 +192,34 @@ func pollAsyncOperation(
}, printer.StructuredOptions{})
case operation.AsyncOperation_STATE_FAILED:
progressString = fmt.Sprintf("[%s] Operation failed: %s\n", time.Now().Format("15:04:05"), asyncOp.FailureReason)
return cctx.Printer.PrintStructured(MutationResult{
// Print the structured output first, then return error for proper exit code
if err := cctx.Printer.PrintStructured(MutationResult{
ID: id,
AsyncOp: asyncOp,
}, printer.StructuredOptions{})
}, printer.StructuredOptions{}); err != nil {
return err
}
return fmt.Errorf("async operation failed: %s", asyncOp.FailureReason)
case operation.AsyncOperation_STATE_CANCELLED:
progressString = fmt.Sprintf("[%s] Operation cancelled\n", time.Now().Format("15:04:05"))
return cctx.Printer.PrintStructured(MutationResult{
// Print the structured output first, then return error for proper exit code
if err := cctx.Printer.PrintStructured(MutationResult{
ID: id,
AsyncOp: asyncOp,
}, printer.StructuredOptions{})
}, printer.StructuredOptions{}); err != nil {
return err
}
return fmt.Errorf("async operation cancelled")
case operation.AsyncOperation_STATE_REJECTED:
progressString = fmt.Sprintf("[%s] Operation rejected\n", time.Now().Format("15:04:05"))
return cctx.Printer.PrintStructured(MutationResult{
// Print the structured output first, then return error for proper exit code
if err := cctx.Printer.PrintStructured(MutationResult{
ID: id,
AsyncOp: asyncOp,
}, printer.StructuredOptions{})
}, printer.StructuredOptions{}); err != nil {
return err
}
return fmt.Errorf("async operation rejected")
default:
progressString = fmt.Sprintf("[%s] Operation pending...\n", time.Now().Format("15:04:05"))
}
Expand Down