feat(instance): add functionality to update instance (#4440)

Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com>
This commit is contained in:
Stefan Benz 2022-09-27 07:58:50 +01:00 committed by GitHub
parent 0755ed8a70
commit b32c02a39b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 339 additions and 5 deletions

View File

@ -57,6 +57,18 @@ This might take some time
POST: /instances POST: /instances
### UpdateInstance
> **rpc** UpdateInstance([UpdateInstanceRequest](#updateinstancerequest))
[UpdateInstanceResponse](#updateinstanceresponse)
Updates name of an existing instance
PUT: /instances/{instance_id}
### RemoveInstance ### RemoveInstance
> **rpc** RemoveInstance([RemoveInstanceRequest](#removeinstancerequest)) > **rpc** RemoveInstance([RemoveInstanceRequest](#removeinstancerequest))
@ -599,6 +611,29 @@ This is an empty response
### UpdateInstanceRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| instance_id | string | - | |
| instance_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### UpdateInstanceResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### View ### View

View File

@ -51,6 +51,17 @@ func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequ
}, nil }, nil
} }
func (s *Server) UpdateInstance(ctx context.Context, req *system_pb.UpdateInstanceRequest) (*system_pb.UpdateInstanceResponse, error) {
ctx = authz.WithInstanceID(ctx, req.InstanceId)
details, err := s.command.UpdateInstance(ctx, req.InstanceName)
if err != nil {
return nil, err
}
return &system_pb.UpdateInstanceResponse{
Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
}, nil
}
func (s *Server) ExistsDomain(ctx context.Context, req *system_pb.ExistsDomainRequest) (*system_pb.ExistsDomainResponse, error) { func (s *Server) ExistsDomain(ctx context.Context, req *system_pb.ExistsDomainRequest) (*system_pb.ExistsDomainResponse, error) {
domainQuery, err := query.NewInstanceDomainDomainSearchQuery(query.TextEqualsIgnoreCase, req.Domain) domainQuery, err := query.NewInstanceDomainDomainSearchQuery(query.TextEqualsIgnoreCase, req.Domain)
if err != nil { if err != nil {

View File

@ -362,6 +362,24 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
}, nil }, nil
} }
func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := c.prepareUpdateInstance(instanceAgg, name)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation)
if err != nil {
return nil, err
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}, nil
}
func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) (*domain.ObjectDetails, error) { func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := c.prepareSetDefaultLanguage(instanceAgg, defaultLanguage) validation := c.prepareSetDefaultLanguage(instanceAgg, defaultLanguage)
@ -376,7 +394,7 @@ func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage langu
return &domain.ObjectDetails{ return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(), Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(), EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().InstanceID, ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}, nil }, nil
} }
@ -394,7 +412,7 @@ func (c *Commands) SetDefaultOrg(ctx context.Context, orgID string) (*domain.Obj
return &domain.ObjectDetails{ return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(), Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(), EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().InstanceID, ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}, nil }, nil
} }
@ -444,7 +462,7 @@ func prepareAddInstance(a *instance.Aggregate, instanceName string, defaultLangu
} }
} }
//SetIAMProject defines the command to set the id of the IAM project onto the instance // SetIAMProject defines the command to set the id of the IAM project onto the instance
func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validation { func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
@ -455,7 +473,7 @@ func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validati
} }
} }
//SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance // SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance
func SetIAMConsoleID(a *instance.Aggregate, clientID, appID *string) preparation.Validation { func SetIAMConsoleID(a *instance.Aggregate, clientID, appID *string) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
@ -498,6 +516,27 @@ func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggrega
return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil
} }
func (c *Commands) prepareUpdateInstance(a *instance.Aggregate, name string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name == "" {
return nil, errors.ThrowInvalidArgument(nil, "INST-092mid", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := getInstanceWriteModel(ctx, filter)
if err != nil {
return nil, err
}
if writeModel.State == domain.InstanceStateUnspecified {
return nil, errors.ThrowNotFound(nil, "INST-nuso2m", "Errors.Instance.NotFound")
}
if writeModel.Name == name {
return nil, errors.ThrowPreconditionFailed(nil, "INST-alpxism", "Errors.Instance.NotChanged")
}
return []eventstore.Command{instance.NewInstanceChangedEvent(ctx, &a.Aggregate, name)}, nil
}, nil
}
}
func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation { func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
if defaultLanguage == language.Und { if defaultLanguage == language.Und {

View File

@ -23,6 +23,7 @@ type InstanceWriteModel struct {
func NewInstanceWriteModel(instanceID string) *InstanceWriteModel { func NewInstanceWriteModel(instanceID string) *InstanceWriteModel {
return &InstanceWriteModel{ return &InstanceWriteModel{
WriteModel: eventstore.WriteModel{ WriteModel: eventstore.WriteModel{
InstanceID: instanceID,
AggregateID: instanceID, AggregateID: instanceID,
ResourceOwner: instanceID, ResourceOwner: instanceID,
}, },

View File

@ -0,0 +1,180 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
)
func TestCommandSide_ChangeInstance(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
name string
instanceID string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "empty name, invalid error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
name: "",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "instance not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
name: "INSTANCE_CHANGED",
},
res: res{
err: caos_errs.IsNotFound,
},
},
/* instance removed is not yet implemented
{
name: "generator removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
eventFromEventPusher(
instance.NewInstanceRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
err: caos_errs.IsNotFound,
},
},*/
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
name: "INSTANCE",
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "instance change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewInstanceAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewInstanceChangedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE_CHANGED",
),
),
},
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.UpdateInstance(tt.args.ctx, tt.args.name)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}

View File

@ -137,6 +137,7 @@ func Test_defaultDomainPolicy(t *testing.T) {
AggregateID: "INSTANCE", AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE", ResourceOwner: "INSTANCE",
Events: []eventstore.Event{}, Events: []eventstore.Event{},
InstanceID: "INSTANCE",
}, },
UserLoginMustBeDomain: true, UserLoginMustBeDomain: true,
ValidateOrgDomains: true, ValidateOrgDomains: true,
@ -248,6 +249,7 @@ func Test_DomainPolicy(t *testing.T) {
AggregateID: "INSTANCE", AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE", ResourceOwner: "INSTANCE",
Events: []eventstore.Event{}, Events: []eventstore.Event{},
InstanceID: "INSTANCE",
}, },
UserLoginMustBeDomain: true, UserLoginMustBeDomain: true,
ValidateOrgDomains: true, ValidateOrgDomains: true,

View File

@ -143,6 +143,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) {
AggregateID: "INSTANCE", AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE", ResourceOwner: "INSTANCE",
Events: []eventstore.Event{}, Events: []eventstore.Event{},
InstanceID: "INSTANCE",
}, },
MinLength: 8, MinLength: 8,
HasLowercase: true, HasLowercase: true,
@ -262,6 +263,7 @@ func Test_passwordComplexityPolicy(t *testing.T) {
AggregateID: "INSTANCE", AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE", ResourceOwner: "INSTANCE",
Events: []eventstore.Event{}, Events: []eventstore.Event{},
InstanceID: "INSTANCE",
}, },
MinLength: 8, MinLength: 8,
HasLowercase: true, HasLowercase: true,

View File

@ -62,6 +62,10 @@ func (p *instanceProjection) reducers() []handler.AggregateReducer {
Event: instance.InstanceAddedEventType, Event: instance.InstanceAddedEventType,
Reduce: p.reduceInstanceAdded, Reduce: p.reduceInstanceAdded,
}, },
{
Event: instance.InstanceChangedEventType,
Reduce: p.reduceInstanceChanged,
},
{ {
Event: instance.DefaultOrgSetEventType, Event: instance.DefaultOrgSetEventType,
Reduce: p.reduceDefaultOrgSet, Reduce: p.reduceDefaultOrgSet,
@ -100,6 +104,24 @@ func (p *instanceProjection) reduceInstanceAdded(event eventstore.Event) (*handl
), nil ), nil
} }
func (p *instanceProjection) reduceInstanceChanged(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*instance.InstanceChangedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-so2am1", "reduce.wrong.event.type %s", instance.InstanceChangedEventType)
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(InstanceColumnName, e.Name),
handler.NewCol(InstanceColumnChangeDate, e.CreationDate()),
handler.NewCol(InstanceColumnSequence, e.Sequence()),
},
[]handler.Condition{
handler.NewCond(InstanceColumnID, e.Aggregate().InstanceID),
},
), nil
}
func (p *instanceProjection) reduceDefaultOrgSet(event eventstore.Event) (*handler.Statement, error) { func (p *instanceProjection) reduceDefaultOrgSet(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*instance.DefaultOrgSetEvent) e, ok := event.(*instance.DefaultOrgSetEvent)
if !ok { if !ok {

View File

@ -20,6 +20,7 @@ type Aggregate struct {
func NewAggregate(instanceID string) *Aggregate { func NewAggregate(instanceID string) *Aggregate {
return &Aggregate{ return &Aggregate{
Aggregate: eventstore.Aggregate{ Aggregate: eventstore.Aggregate{
InstanceID: instanceID,
Type: AggregateType, Type: AggregateType,
Version: AggregateVersion, Version: AggregateVersion,
ID: instanceID, ID: instanceID,

View File

@ -66,7 +66,7 @@ func (e *InstanceChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCons
return nil return nil
} }
func NewInstanceChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, oldName, newName string) *InstanceChangedEvent { func NewInstanceChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, newName string) *InstanceChangedEvent {
return &InstanceChangedEvent{ return &InstanceChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
ctx, ctx,

View File

@ -142,6 +142,10 @@ Errors:
RefreshToken: RefreshToken:
Invalid: Refresh Token ist ungültig Invalid: Refresh Token ist ungültig
NotFound: Refresh Token nicht gefunden NotFound: Refresh Token nicht gefunden
Instance:
NotFound: Instanz konnte nicht gefunden werden
AlreadyExists: Instanz exisitiert bereits
NotChanged: Instanz wurde nicht verändert
Org: Org:
AlreadyExists: Organisationsname existiert bereits AlreadyExists: Organisationsname existiert bereits
Invalid: Organisation ist ungültig Invalid: Organisation ist ungültig

View File

@ -142,6 +142,10 @@ Errors:
RefreshToken: RefreshToken:
Invalid: Refresh Token is invalid Invalid: Refresh Token is invalid
NotFound: Refresh Token not found NotFound: Refresh Token not found
Instance:
NotFound: Instance not found
AlreadyExists: Instance already exists
NotChanged: Instance not changed
Org: Org:
AlreadyExists: Organisation's name already taken AlreadyExists: Organisation's name already taken
Invalid: Organisation is invalid Invalid: Organisation is invalid

View File

@ -142,6 +142,10 @@ Errors:
RefreshToken: RefreshToken:
Invalid: Le jeton de rafraîchissement n'est pas valide Invalid: Le jeton de rafraîchissement n'est pas valide
NotFound: Jeton de rafraîchissement non trouvé NotFound: Jeton de rafraîchissement non trouvé
Instance:
NotFound: Instance non trouvée
AlreadyExists: L'instance existe déjà
NotChanged: L'instance n'a pas changé
Org: Org:
AlreadyExists: Le nom de l'organisation est déjà pris AlreadyExists: Le nom de l'organisation est déjà pris
Invalid: L'organisation n'est pas valide Invalid: L'organisation n'est pas valide

View File

@ -142,6 +142,10 @@ Errors:
RefreshToken: RefreshToken:
Invalid: Refresh Token non è valido Invalid: Refresh Token non è valido
NotFound: Refresh Token non trovato NotFound: Refresh Token non trovato
Instance:
NotFound: Istanza non trovata
AlreadyExists: L'istanza esiste già
NotChanged: Istanza non modificata
Org: Org:
AlreadyExists: Nome dell'organizzazione già preso AlreadyExists: Nome dell'organizzazione già preso
Invalid: L'organizzazione non è valida Invalid: L'organizzazione non è valida

View File

@ -142,6 +142,10 @@ Errors:
RefreshToken: RefreshToken:
Invalid: Refresh Token 无效 Invalid: Refresh Token 无效
NotFound: 未找到 Refresh Token NotFound: 未找到 Refresh Token
Instance:
NotFound: 没有找到实例
AlreadyExists: 实例已经存在
NotChanged: 实例没有改变
Org: Org:
AlreadyExists: 组织名称已被占用 AlreadyExists: 组织名称已被占用
Invalid: 组织无效 Invalid: 组织无效

View File

@ -134,6 +134,18 @@ service SystemService {
}; };
} }
// Updates name of an existing instance
rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) {
option (google.api.http) = {
put: "/instances/{instance_id}"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated";
};
}
// Removes a instances // Removes a instances
// This might take some time // This might take some time
rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) {
@ -397,6 +409,15 @@ message AddInstanceResponse {
zitadel.v1.ObjectDetails details = 2; zitadel.v1.ObjectDetails details = 2;
} }
message UpdateInstanceRequest{
string instance_id = 1;
string instance_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message UpdateInstanceResponse{
zitadel.v1.ObjectDetails details = 1;
}
message RemoveInstanceRequest { message RemoveInstanceRequest {
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
} }