Skip to content
Open
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
2 changes: 2 additions & 0 deletions internal/namespace/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ type GetNamespacesParams struct {
PageSize int32
PageToken string
Name string
ProjectID string
}

func (c *Client) GetNamespaces(ctx context.Context, params GetNamespacesParams) ([]*namespacev1.Namespace, string, error) {
res, err := c.Cloud.GetNamespaces(ctx, &cloudservice.GetNamespacesRequest{
PageSize: params.PageSize,
PageToken: params.PageToken,
Name: params.Name,
ProjectId: params.ProjectID,
})
if err != nil {
return nil, "", err
Expand Down
40 changes: 40 additions & 0 deletions internal/namespace/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package namespace_test

import (
"context"
"testing"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1"
namespacev1 "go.temporal.io/cloud-sdk/api/namespace/v1"

"github.com/temporalio/cloud-cli/internal/namespace"
namespacemock "github.com/temporalio/cloud-cli/internal/namespace/mock"
)

func TestClientGetNamespaces_ProjectID(t *testing.T) {
cloud := namespacemock.NewMockCloudService(t)
client := &namespace.Client{Cloud: cloud}

cloud.EXPECT().
GetNamespaces(mock.Anything, &cloudservice.GetNamespacesRequest{
PageSize: 50,
PageToken: "page-1",
Name: "payments",
ProjectId: "project-123",
}, mock.Anything).
Return(&cloudservice.GetNamespacesResponse{
Namespaces: []*namespacev1.Namespace{{Namespace: "payments.account"}},
}, nil)

namespaces, nextPageToken, err := client.GetNamespaces(context.Background(), namespace.GetNamespacesParams{
PageSize: 50,
PageToken: "page-1",
Name: "payments",
ProjectID: "project-123",
})
require.NoError(t, err)
require.Empty(t, nextPageToken)
require.Len(t, namespaces, 1)
}
14 changes: 9 additions & 5 deletions temporalcloudcli/commands.gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -1984,6 +1984,7 @@ type CloudNamespaceApplyCommand struct {
ClientOptions
DiffOptions
Spec string
ProjectId string
AsyncOperationId string
Idempotent bool
Async bool
Expand All @@ -2004,6 +2005,7 @@ func NewCloudNamespaceApplyCommand(cctx *CommandContext, parent *CloudNamespaceC
s.Command.Args = cobra.NoArgs
s.Command.Flags().StringVar(&s.Spec, "spec", "", "Namespace configuration in JSON format. Provide inline JSON directly, or use '@path/to/file.json' to load from a file. Required.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "spec")
s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to create the namespace in. If omitted, the namespace is created in the account's default project.")
s.Command.Flags().StringVar(&s.AsyncOperationId, "async-operation-id", "", "Custom identifier for tracking this async operation. If not provided, a unique ID is generated automatically.")
s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the namespace already matches the specification. Without this flag, the command errors when no changes are needed.")
s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.")
Expand Down Expand Up @@ -2363,6 +2365,7 @@ type CloudNamespaceCreateCommand struct {
EnableTaskQueueFairness bool
SearchAttribute []string
ConnectionRuleId []string
ProjectId string
}

func NewCloudNamespaceCreateCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceCreateCommand {
Expand All @@ -2388,6 +2391,7 @@ func NewCloudNamespaceCreateCommand(cctx *CommandContext, parent *CloudNamespace
s.Command.Flags().BoolVar(&s.EnableTaskQueueFairness, "enable-task-queue-fairness", false, "Enable task queue fairness for the namespace.")
s.Command.Flags().StringArrayVar(&s.SearchAttribute, "search-attribute", nil, "Custom search attribute as 'name=Type' (e.g. --search-attribute myAttr=Keyword). Valid types: Text, Keyword, Int, Double, Bool, Datetime, KeywordList. Repeat to add multiple.")
s.Command.Flags().StringArrayVar(&s.ConnectionRuleId, "connection-rule-id", nil, "Private connectivity rule ID. Repeat to specify multiple.")
s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to create the namespace in. If omitted, the namespace is created in the account's default project.")
s.ClientOptions.BuildFlags(s.Command.Flags())
s.AsyncOperationOptions.BuildFlags(s.Command.Flags())
s.CodecServerOptions.BuildFlags(s.Command.Flags())
Expand Down Expand Up @@ -3377,6 +3381,7 @@ type CloudNamespaceListCommand struct {
PageSize int
PageToken string
Name string
ProjectId string
}

func NewCloudNamespaceListCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceListCommand {
Expand All @@ -3394,6 +3399,7 @@ func NewCloudNamespaceListCommand(cctx *CommandContext, parent *CloudNamespaceCo
s.Command.Flags().IntVar(&s.PageSize, "page-size", 0, "Number of namespaces to return per page. Use for paginated results.")
s.Command.Flags().StringVar(&s.PageToken, "page-token", "", "Token for retrieving the next page of results in a paginated list.")
s.Command.Flags().StringVar(&s.Name, "name", "", "Filter namespaces by the name as defined in the specification of the namespace.")
s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "Filter namespaces by project ID.")
s.ClientOptions.BuildFlags(s.Command.Flags())
s.Command.Run = func(c *cobra.Command, args []string) {
if err := s.run(cctx, args); err != nil {
Expand Down Expand Up @@ -4654,8 +4660,7 @@ type CloudProjectApplyCommand struct {
DiffOptions
AsyncOperationOptions
ResourceVersionOptions
ProjectId string
Spec string
Spec string
}

func NewCloudProjectApplyCommand(cctx *CommandContext, parent *CloudProjectCommand) *CloudProjectApplyCommand {
Expand All @@ -4665,12 +4670,11 @@ func NewCloudProjectApplyCommand(cctx *CommandContext, parent *CloudProjectComma
s.Command.Use = "apply [flags]"
s.Command.Short = "Create or update a project from a specification"
if hasHighlighting {
s.Command.Long = "Apply a project configuration to Temporal Cloud. If --project-id is\nprovided, the existing project is updated. Otherwise, a new project is\ncreated because project IDs are generated by the server.\n\nThe specification can be provided as inline JSON or loaded from a file\nby prefixing the path with '@'.\n\nExample:\n\n\x1b[1mtemporal cloud project apply --spec '{\"display_name\": \"Engineering\", \"description\": \"Engineering workloads\"}'\x1b[0m"
s.Command.Long = "Apply a project configuration to Temporal Cloud. An existing project with\nthe specification's display_name is updated if found; if no matching\nproject exists, a new project is created.\n\nThe specification can be provided as inline JSON or loaded from a file\nby prefixing the path with '@'.\n\nExample:\n\n\x1b[1mtemporal cloud project apply --spec '{\"display_name\": \"Engineering\", \"description\": \"Engineering workloads\"}'\x1b[0m"
} else {
s.Command.Long = "Apply a project configuration to Temporal Cloud. If --project-id is\nprovided, the existing project is updated. Otherwise, a new project is\ncreated because project IDs are generated by the server.\n\nThe specification can be provided as inline JSON or loaded from a file\nby prefixing the path with '@'.\n\nExample:\n\n```\ntemporal cloud project apply --spec '{\"display_name\": \"Engineering\", \"description\": \"Engineering workloads\"}'\n```"
s.Command.Long = "Apply a project configuration to Temporal Cloud. An existing project with\nthe specification's display_name is updated if found; if no matching\nproject exists, a new project is created.\n\nThe specification can be provided as inline JSON or loaded from a file\nby prefixing the path with '@'.\n\nExample:\n\n```\ntemporal cloud project apply --spec '{\"display_name\": \"Engineering\", \"description\": \"Engineering workloads\"}'\n```"
}
s.Command.Args = cobra.NoArgs
s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to update. Omit to create a new project.")
s.Command.Flags().StringVar(&s.Spec, "spec", "", "Project configuration in JSON format. Provide inline JSON directly, or use '@path/to/file.json' to load from a file. Required.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "spec")
s.ClientOptions.BuildFlags(s.Command.Flags())
Expand Down
39 changes: 39 additions & 0 deletions temporalcloudcli/commands.namespace.create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ func createReqMatcher(expected *namespacev1.NamespaceSpec) interface{} {
})
}

func createReqMatcherWithProject(expected *namespacev1.NamespaceSpec, projectID string) interface{} {
return mock.MatchedBy(func(req *cloudservice.CreateNamespaceRequest) bool {
return proto.Equal(req.Spec, expected) && req.ProjectId == projectID
})
}

// TestCreateNamespace_Success verifies that CreateNamespace calls HandleOperation with the API response.
func TestCreateNamespace_Success(t *testing.T) {
mockCloud := cloudmock.NewMockCloudServiceClient(t)
Expand Down Expand Up @@ -84,6 +90,39 @@ func TestCreateNamespace_Success(t *testing.T) {
require.NoError(t, err)
}

func TestCreateNamespace_ProjectID(t *testing.T) {
mockCloud := cloudmock.NewMockCloudServiceClient(t)
mockPrompter := cmdmock.NewMockPrompter(t)
mockHandler := cmdmock.NewMockAsyncOperationHandler(t)

spec := baseNamespaceSpec()

mockPrompter.EXPECT().
PromptApply(&namespacev1.NamespaceSpec{}, specMatcher(spec), false).
Return(nil)

mockCloud.EXPECT().
CreateNamespace(context.Background(), createReqMatcherWithProject(spec, "project-123")).
Return(defaultCreateResponse, nil)

mockHandler.EXPECT().
HandleOperation(defaultCreateResponse.AsyncOperation, "my-namespace.my-account").
Return(nil)

var buf bytes.Buffer
err := temporalcloudcli.CreateNamespace(context.Background(), temporalcloudcli.CreateNamespaceParams{
Name: "my-namespace",
Regions: []string{"aws-us-east-1"},
ProjectID: "project-123",
Cloud: mockCloud,
Printer: &printer.Printer{Output: &buf, JSON: true},
Prompter: mockPrompter,
UnmarshalProtoJSON: noopUnmarshalProtoJSON,
OperationHandler: mockHandler,
})
require.NoError(t, err)
}

// TestCreateNamespace_BuildsSpec verifies that CreateNamespace correctly wires params into the NamespaceSpec.
func TestCreateNamespace_BuildsSpec(t *testing.T) {
expectedSpec := &namespacev1.NamespaceSpec{
Expand Down
12 changes: 10 additions & 2 deletions temporalcloudcli/commands.namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error
return err
}
client := newNamespaceClient(withCloudClient(cloudClient))
projectID := c.ProjectId

// Step 4: Retrieve existing namespace
var found bool
existing, err := client.getNamespaceByName(cctx.Context, spec.Name)
existing, err := client.getNamespaceByName(cctx.Context, spec.Name, projectID)
if err != nil && !isNotFoundErr(err) {
return err
} else if err == nil {
Expand Down Expand Up @@ -178,6 +179,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error
res, err := client.createNamespace(cctx.Context, createNamespaceParams{
spec: spec,
asyncOperationID: c.AsyncOperationId,
projectID: projectID,
})
if err != nil {
return fmt.Errorf("failed to create namespace: %w", err)
Expand Down Expand Up @@ -281,11 +283,13 @@ func (c *CloudNamespaceListCommand) run(cctx *CommandContext, _ []string) error
}

client := newNamespaceClient(withCloudClient(cloudClient))
projectID := c.ProjectId

namespaces, nextPageToken, err := client.getNamespaces(cctx.Context, getNamespacesParams{
pageSize: int32(c.PageSize),
pageToken: c.PageToken,
name: c.Name,
projectID: projectID,
})
if err != nil {
return err
Expand All @@ -300,7 +304,7 @@ func (c *CloudNamespaceListCommand) run(cctx *CommandContext, _ []string) error
NextPageToken: nextPageToken,
},
printer.PrintResourceOptions{
Fields: []string{"Namespace", "State", "CreatedTime"},
Fields: []string{"Namespace", "ProjectId", "State", "CreatedTime"},
SpecFields: []string{"Regions"},
},
printer.TableOptions{},
Expand All @@ -324,6 +328,7 @@ type (
CodecPassAccessToken bool
CodecIncludeCrossOriginCredentials bool
ConnectionRuleIDs []string
ProjectID string

Cloud cloudservice.CloudServiceClient
Printer *printer.Printer
Expand Down Expand Up @@ -425,6 +430,7 @@ func CreateNamespace(ctx context.Context, params CreateNamespaceParams) error {
return createNamespace(ctx, &cloudservice.CreateNamespaceRequest{
Spec: spec,
AsyncOperationId: params.AsyncOperationID,
ProjectId: params.ProjectID,
})
}

Expand All @@ -440,6 +446,7 @@ func (c *CloudNamespaceCreateCommand) run(cctx *CommandContext, _ []string) erro
if c.Command.Flags().Changed("enable-task-queue-fairness") {
enableTaskQueueFairness = &c.EnableTaskQueueFairness
}
projectID := c.ProjectId

return CreateNamespace(cctx.Context, CreateNamespaceParams{
Name: c.Name,
Expand All @@ -457,6 +464,7 @@ func (c *CloudNamespaceCreateCommand) run(cctx *CommandContext, _ []string) erro
CodecPassAccessToken: c.CodecPassAccessToken,
CodecIncludeCrossOriginCredentials: c.CodecIncludeCrossOriginCredentials,
ConnectionRuleIDs: c.ConnectionRuleId,
ProjectID: projectID,
Cloud: cloudClient.CloudService(),
Printer: cctx.Printer,
Prompter: newPrompter(cctx),
Expand Down
Loading
Loading