Skip to content

Commit 36446a7

Browse files
feat: don't error on no changes, just log (#448)
* feat: don't error on no changes, just log * fix: use a flag for idempotentcy * fix: tests * feat: NS and user group idempotent flag
1 parent 91c5661 commit 36446a7

10 files changed

Lines changed: 166 additions & 3 deletions

File tree

app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func NewApp(params AppParams) (*cli.App, error) {
2323
ServerFlag,
2424
ConfigDirFlag,
2525
AutoConfirmFlag,
26+
IdempotentFlag,
2627
APIKeyFlag,
2728
InsecureConnectionFlag,
2829
EnableDebugLogsFlag,

app/common.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"strings"
88

99
apipayload "github.com/temporalio/tcld/protogen/temporal/api/common/v1"
10+
"github.com/urfave/cli/v2"
11+
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
1013
)
1114

1215
const (
@@ -35,6 +38,19 @@ func IsFeatureEnabled(feature string) bool {
3538
return false
3639
}
3740

41+
func isNothingChangedErr(ctx *cli.Context, e error) bool {
42+
// If we are not idempotent, we should error on nothing to change
43+
if !ctx.Bool(IdempotentFlagName) {
44+
return false
45+
}
46+
47+
s, ok := status.FromError(e)
48+
if !ok {
49+
return false
50+
}
51+
return s.Code() == codes.InvalidArgument && strings.Contains(s.Message(), "nothing to change")
52+
}
53+
3854
func parseAssumedRole(assumedRole string) (string, string, error) {
3955
var accountID, roleName string
4056
re := assumedRolePattern

app/flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const (
1717
APIKeyFlagName = "api-key"
1818
InsecureConnectionFlagName = "insecure"
1919
EnableDebugLogsFlagName = "enable-debug-logs"
20+
IdempotentFlagName = "idempotent"
2021
AuthenticationFlagCategory = "Authentication:"
2122

2223
// APIKeyVersionTag indicates the state of API keys. This should be removed when fully released.
@@ -86,4 +87,9 @@ var (
8687
Usage: "A flag to enable debug logs",
8788
EnvVars: []string{"TEMPORAL_CLOUD_ENABLE_DEBUG_LOGS"},
8889
}
90+
IdempotentFlag = &cli.BoolFlag{
91+
Name: IdempotentFlagName,
92+
Usage: "A flag to not error if there are no changes to the resource",
93+
EnvVars: []string{"TEMPORAL_CLOUD_IDEMPOTENT"},
94+
}
8995
)

app/namespace.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ func (c *NamespaceClient) updateNamespace(ctx *cli.Context, n *namespace.Namespa
344344
Spec: n.Spec,
345345
})
346346
if err != nil {
347+
if isNothingChangedErr(ctx, err) {
348+
return nil
349+
}
347350
return err
348351
}
349352

@@ -802,13 +805,20 @@ func NewNamespaceCommand(getNamespaceClientFn GetNamespaceClientFn) (CommandOut,
802805

803806
if enable {
804807
if n.Spec.Lifecycle != nil && n.Spec.Lifecycle.EnableDeleteProtection {
808+
if ctx.Bool(IdempotentFlagName) {
809+
return nil
810+
}
811+
805812
return errors.New("delete protection is already enabled")
806813
}
807814
n.Spec.Lifecycle = &namespace.LifecycleSpec{
808815
EnableDeleteProtection: true,
809816
}
810817
} else {
811818
if n.Spec.Lifecycle == nil || !n.Spec.Lifecycle.EnableDeleteProtection {
819+
if ctx.Bool(IdempotentFlagName) {
820+
return nil
821+
}
812822
return errors.New("delete protection is already disabled")
813823
}
814824
y, err := ConfirmPrompt(ctx, "disabling namespace delete protection may be prone to accidental deletion. confirm?")
@@ -936,6 +946,9 @@ func NewNamespaceCommand(getNamespaceClientFn GetNamespaceClientFn) (CommandOut,
936946
return err
937947
}
938948
if n.Spec.AcceptedClientCa == bundle {
949+
if ctx.Bool(IdempotentFlagName) {
950+
return nil
951+
}
939952
return errors.New("nothing to change")
940953
}
941954
n.Spec.AcceptedClientCa = bundle
@@ -983,6 +996,9 @@ func NewNamespaceCommand(getNamespaceClientFn GetNamespaceClientFn) (CommandOut,
983996
return err
984997
}
985998
if n.Spec.AcceptedClientCa == bundle {
999+
if ctx.Bool(IdempotentFlagName) {
1000+
return nil
1001+
}
9861002
return errors.New("nothing to change")
9871003
}
9881004
n.Spec.AcceptedClientCa = bundle
@@ -1014,6 +1030,9 @@ func NewNamespaceCommand(getNamespaceClientFn GetNamespaceClientFn) (CommandOut,
10141030
return err
10151031
}
10161032
if n.Spec.AcceptedClientCa == cert {
1033+
if ctx.Bool(IdempotentFlagName) {
1034+
return nil
1035+
}
10171036
return errors.New("nothing to change")
10181037
}
10191038
n.Spec.AcceptedClientCa = cert
@@ -1051,6 +1070,9 @@ func NewNamespaceCommand(getNamespaceClientFn GetNamespaceClientFn) (CommandOut,
10511070
return err
10521071
}
10531072
if n.Spec.AuthMethod == authMethod {
1073+
if ctx.Bool(IdempotentFlagName) {
1074+
return nil
1075+
}
10541076
return errors.New("nothing to change")
10551077
}
10561078
if disruptiveChange(n.Spec.AuthMethod, authMethod) {

app/namespace_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func (s *NamespaceTestSuite) SetupTest() {
6565
}
6666
flags := []cli.Flag{
6767
AutoConfirmFlag,
68+
IdempotentFlag,
6869
}
6970

7071
s.cliApp, _ = NewTestApp(s.T(), cmds, flags)
@@ -224,6 +225,16 @@ func (s *NamespaceTestSuite) TestDeleteProtection() {
224225
},
225226
expectErr: true,
226227
},
228+
{
229+
name: "no change already enabled, idempotent",
230+
args: []string{"--idempotent", "n", "lc", "set", "-n", ns, "--edp", "true"},
231+
expectGet: func(g *namespaceservice.GetNamespaceResponse) {
232+
g.Namespace.Spec.Lifecycle = &namespace.LifecycleSpec{
233+
EnableDeleteProtection: true,
234+
}
235+
},
236+
expectErr: false,
237+
},
227238
{
228239
name: "no change already disabled",
229240
args: []string{"n", "lc", "set", "-n", ns, "--edp", "false"},

app/serviceaccount.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ func (c *ServiceAccountClient) performUpdate(
183183

184184
resp, err := c.client.UpdateServiceAccount(c.ctx, req)
185185
if err != nil {
186+
if isNothingChangedErr(ctx, err) {
187+
return nil
188+
}
186189
return fmt.Errorf("unable to update service account: %w", err)
187190
}
188191
return PrintProto(resp.RequestStatus)

app/serviceaccount_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/temporalio/tcld/protogen/api/request/v1"
1414
authservicemock "github.com/temporalio/tcld/protogen/apimock/authservice/v1"
1515
"github.com/urfave/cli/v2"
16+
"google.golang.org/grpc/codes"
17+
"google.golang.org/grpc/status"
1618
)
1719

1820
func TestServiceAccount(t *testing.T) {
@@ -42,6 +44,7 @@ func (s *ServiceAccountTestSuite) SetupTest() {
4244
Commands: []*cli.Command{out.Command},
4345
Flags: []cli.Flag{
4446
AutoConfirmFlag,
47+
IdempotentFlag,
4548
},
4649
}
4750
}
@@ -354,3 +357,41 @@ func (s *ServiceAccountTestSuite) TestSetNamespacePermissionsEmpty() {
354357
}, nil)
355358
s.NoError(s.RunCmd("service-account", "set-namespace-permissions", "--service-account-id", "test-service-account-id"))
356359
}
360+
361+
func (s *ServiceAccountTestSuite) TestSetNamespacePermissionsNoChange() {
362+
s.mockAuthService.EXPECT().GetServiceAccount(gomock.Any(), gomock.Any()).Return(&authservice.GetServiceAccountResponse{
363+
ServiceAccount: &auth.ServiceAccount{
364+
Id: "test-service-account-id",
365+
Spec: &auth.ServiceAccountSpec{
366+
Name: "test-service-account-name",
367+
Description: "test-service-account-desc",
368+
Access: &auth.Access{
369+
AccountAccess: &auth.AccountAccess{
370+
Role: auth.ACCOUNT_ACTION_GROUP_READ,
371+
},
372+
NamespaceAccesses: map[string]*auth.NamespaceAccess{},
373+
},
374+
},
375+
},
376+
}, nil).Times(1)
377+
s.mockAuthService.EXPECT().UpdateServiceAccount(gomock.Any(), gomock.All(&updateServiceAccountRequestMatcher{
378+
request: &authservice.UpdateServiceAccountRequest{
379+
ServiceAccountId: "test-service-account-id",
380+
Spec: &auth.ServiceAccountSpec{
381+
Name: "test-service-account-name",
382+
Description: "test-service-account-desc",
383+
Access: &auth.Access{
384+
AccountAccess: &auth.AccountAccess{
385+
Role: auth.ACCOUNT_ACTION_GROUP_READ,
386+
},
387+
NamespaceAccesses: map[string]*auth.NamespaceAccess{
388+
"test-namespace-1": {
389+
Permission: auth.NAMESPACE_ACTION_GROUP_READ,
390+
},
391+
},
392+
},
393+
},
394+
},
395+
})).Return(nil, status.Error(codes.InvalidArgument, "nothing to change")).Times(1)
396+
s.NoError(s.RunCmd("--idempotent", "service-account", "set-namespace-permissions", "--service-account-id", "test-service-account-id", "-p", "test-namespace-1=Read"))
397+
}

app/user.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ func (c *UserClient) performUpdate(ctx *cli.Context, user *auth.User) error {
241241
}
242242
resp, err := c.client.UpdateUser(c.ctx, req)
243243
if err != nil {
244+
if isNothingChangedErr(ctx, err) {
245+
return nil
246+
}
244247
return fmt.Errorf("unable to update user: %w", err)
245248
}
246249
return PrintProto(resp.GetRequestStatus())

app/user_group.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func nsRoleToAccess(role string) (string, *identity.NamespaceAccess) {
127127
}
128128

129129
// setAccess sets the access for a group using the cloud services API
130-
func (c *UserGroupClient) setAccess(_ *cli.Context, groupID string, accountRole string, nsRoles []string) error {
130+
func (c *UserGroupClient) setAccess(ctx *cli.Context, groupID string, accountRole string, nsRoles []string) error {
131131
group, err := c.client.GetUserGroup(c.ctx, &cloudsvc.GetUserGroupRequest{
132132
GroupId: groupID,
133133
})
@@ -162,6 +162,9 @@ func (c *UserGroupClient) setAccess(_ *cli.Context, groupID string, accountRole
162162

163163
resp, err := c.client.UpdateUserGroup(c.ctx, req)
164164
if err != nil {
165+
if isNothingChangedErr(ctx, err) {
166+
return nil
167+
}
165168
return err
166169
}
167170

app/user_test.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ package app
33
import (
44
"context"
55
"errors"
6+
"reflect"
7+
"testing"
8+
69
"github.com/golang/mock/gomock"
710
"github.com/stretchr/testify/suite"
811
"github.com/temporalio/tcld/protogen/api/auth/v1"
912
"github.com/temporalio/tcld/protogen/api/authservice/v1"
1013
"github.com/temporalio/tcld/protogen/api/request/v1"
1114
authservicemock "github.com/temporalio/tcld/protogen/apimock/authservice/v1"
1215
"github.com/urfave/cli/v2"
13-
"reflect"
14-
"testing"
16+
"google.golang.org/grpc/codes"
17+
"google.golang.org/grpc/status"
1518
)
1619

1720
func TestUser(t *testing.T) {
@@ -41,6 +44,7 @@ func (s *UserTestSuite) SetupTest() {
4144
}
4245
flags := []cli.Flag{
4346
AutoConfirmFlag,
47+
IdempotentFlag,
4448
}
4549
s.cliApp, _ = NewTestApp(s.T(), cmds, flags)
4650
}
@@ -543,3 +547,56 @@ func (s *UserTestSuite) TestSetNamespacePermissionsEmpty() {
543547
}, nil)
544548
s.NoError(s.RunCmd("user", "set-namespace-permissions", "--user-email", "test@example.com"))
545549
}
550+
551+
func (s *UserTestSuite) TestSetNamespacePermissionsNoChanges() {
552+
s.mockAuthService.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(&authservice.GetUserResponse{
553+
User: &auth.User{
554+
Id: "test-user-id",
555+
Spec: &auth.UserSpec{
556+
Email: "test@example.com",
557+
},
558+
},
559+
}, nil).Times(1)
560+
s.mockAuthService.EXPECT().GetRoles(gomock.Any(), gomock.Any()).Return(&authservice.GetRolesResponse{
561+
Roles: []*auth.Role{
562+
{
563+
Id: "test-account-developer-role",
564+
Type: auth.ROLE_TYPE_PREDEFINED,
565+
Spec: &auth.RoleSpec{
566+
AccountRole: &auth.AccountRoleSpec{
567+
ActionGroup: auth.ACCOUNT_ACTION_GROUP_DEVELOPER,
568+
},
569+
},
570+
},
571+
},
572+
}, nil).Times(1)
573+
s.mockAuthService.EXPECT().GetRolesByPermissions(gomock.Any(), gomock.Any()).Return(&authservice.GetRolesByPermissionsResponse{
574+
Roles: []*auth.Role{
575+
{
576+
Id: "test-ns1-admin-role",
577+
Type: auth.ROLE_TYPE_PREDEFINED,
578+
Spec: &auth.RoleSpec{
579+
NamespaceRoles: []*auth.NamespaceRoleSpec{
580+
{
581+
Namespace: "ns1",
582+
ActionGroup: auth.NAMESPACE_ACTION_GROUP_ADMIN,
583+
},
584+
},
585+
},
586+
},
587+
},
588+
}, nil).Times(1)
589+
s.mockAuthService.EXPECT().UpdateUser(gomock.Any(), gomock.All(&updateUserRequestMatcher{
590+
request: &authservice.UpdateUserRequest{
591+
UserId: "test-user-id",
592+
Spec: &auth.UserSpec{
593+
Email: "test@example.com",
594+
Roles: []string{
595+
"test-account-developer-role",
596+
"test-ns1-admin-role",
597+
},
598+
},
599+
},
600+
})).Return(nil, status.Error(codes.InvalidArgument, "nothing to change")).Times(1)
601+
s.NoError(s.RunCmd("--idempotent", "user", "set-namespace-permissions", "--user-email", "test@example.com", "-p", "ns1=Admin", "-p", "ns2=Write", "-p", "ns3=Read"))
602+
}

0 commit comments

Comments
 (0)