diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md
index 8574121251..ebe4e80a66 100644
--- a/docs/docs/apis/proto/system.md
+++ b/docs/docs/apis/proto/system.md
@@ -57,6 +57,18 @@ This might take some time
POST: /instances
+### UpdateInstance
+
+> **rpc** UpdateInstance([UpdateInstanceRequest](#updateinstancerequest))
+[UpdateInstanceResponse](#updateinstanceresponse)
+
+Updates name of an existing instance
+
+
+
+ PUT: /instances/{instance_id}
+
+
### RemoveInstance
> **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
string.max_len: 200
|
+
+
+
+
+### UpdateInstanceResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### View
diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go
index daeaf54cab..668d903f24 100644
--- a/internal/api/grpc/system/instance.go
+++ b/internal/api/grpc/system/instance.go
@@ -51,6 +51,17 @@ func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequ
}, 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) {
domainQuery, err := query.NewInstanceDomainDomainSearchQuery(query.TextEqualsIgnoreCase, req.Domain)
if err != nil {
diff --git a/internal/command/instance.go b/internal/command/instance.go
index f99e68d12b..c6761c0b0e 100644
--- a/internal/command/instance.go
+++ b/internal/command/instance.go
@@ -362,6 +362,24 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
}, 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) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := c.prepareSetDefaultLanguage(instanceAgg, defaultLanguage)
@@ -376,7 +394,7 @@ func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage langu
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
- ResourceOwner: events[len(events)-1].Aggregate().InstanceID,
+ ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}, nil
}
@@ -394,7 +412,7 @@ func (c *Commands) SetDefaultOrg(ctx context.Context, orgID string) (*domain.Obj
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
- ResourceOwner: events[len(events)-1].Aggregate().InstanceID,
+ ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}, 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 {
return func() (preparation.CreateCommands, 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 {
return func() (preparation.CreateCommands, 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
}
+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 {
return func() (preparation.CreateCommands, error) {
if defaultLanguage == language.Und {
diff --git a/internal/command/instance_model.go b/internal/command/instance_model.go
index 1098a3b092..17437ec1a6 100644
--- a/internal/command/instance_model.go
+++ b/internal/command/instance_model.go
@@ -23,6 +23,7 @@ type InstanceWriteModel struct {
func NewInstanceWriteModel(instanceID string) *InstanceWriteModel {
return &InstanceWriteModel{
WriteModel: eventstore.WriteModel{
+ InstanceID: instanceID,
AggregateID: instanceID,
ResourceOwner: instanceID,
},
diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go
new file mode 100644
index 0000000000..5aa88231d7
--- /dev/null
+++ b/internal/command/instance_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/internal/command/user_domain_policy_test.go b/internal/command/user_domain_policy_test.go
index d8f7e0cd35..ed4a775729 100644
--- a/internal/command/user_domain_policy_test.go
+++ b/internal/command/user_domain_policy_test.go
@@ -137,6 +137,7 @@ func Test_defaultDomainPolicy(t *testing.T) {
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",
Events: []eventstore.Event{},
+ InstanceID: "INSTANCE",
},
UserLoginMustBeDomain: true,
ValidateOrgDomains: true,
@@ -248,6 +249,7 @@ func Test_DomainPolicy(t *testing.T) {
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",
Events: []eventstore.Event{},
+ InstanceID: "INSTANCE",
},
UserLoginMustBeDomain: true,
ValidateOrgDomains: true,
diff --git a/internal/command/user_password_complexity_policy_test.go b/internal/command/user_password_complexity_policy_test.go
index 4dba815ebf..e4d79ab746 100644
--- a/internal/command/user_password_complexity_policy_test.go
+++ b/internal/command/user_password_complexity_policy_test.go
@@ -143,6 +143,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) {
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",
Events: []eventstore.Event{},
+ InstanceID: "INSTANCE",
},
MinLength: 8,
HasLowercase: true,
@@ -262,6 +263,7 @@ func Test_passwordComplexityPolicy(t *testing.T) {
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",
Events: []eventstore.Event{},
+ InstanceID: "INSTANCE",
},
MinLength: 8,
HasLowercase: true,
diff --git a/internal/query/projection/instance.go b/internal/query/projection/instance.go
index df384917f9..4212513ab6 100644
--- a/internal/query/projection/instance.go
+++ b/internal/query/projection/instance.go
@@ -62,6 +62,10 @@ func (p *instanceProjection) reducers() []handler.AggregateReducer {
Event: instance.InstanceAddedEventType,
Reduce: p.reduceInstanceAdded,
},
+ {
+ Event: instance.InstanceChangedEventType,
+ Reduce: p.reduceInstanceChanged,
+ },
{
Event: instance.DefaultOrgSetEventType,
Reduce: p.reduceDefaultOrgSet,
@@ -100,6 +104,24 @@ func (p *instanceProjection) reduceInstanceAdded(event eventstore.Event) (*handl
), 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) {
e, ok := event.(*instance.DefaultOrgSetEvent)
if !ok {
diff --git a/internal/repository/instance/aggregate.go b/internal/repository/instance/aggregate.go
index 4b7dcd3774..4c320dbded 100644
--- a/internal/repository/instance/aggregate.go
+++ b/internal/repository/instance/aggregate.go
@@ -20,6 +20,7 @@ type Aggregate struct {
func NewAggregate(instanceID string) *Aggregate {
return &Aggregate{
Aggregate: eventstore.Aggregate{
+ InstanceID: instanceID,
Type: AggregateType,
Version: AggregateVersion,
ID: instanceID,
diff --git a/internal/repository/instance/instance.go b/internal/repository/instance/instance.go
index 8e11b02150..2e24684c59 100644
--- a/internal/repository/instance/instance.go
+++ b/internal/repository/instance/instance.go
@@ -66,7 +66,7 @@ func (e *InstanceChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCons
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{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index 09a812a784..1c6aa15cc6 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -142,6 +142,10 @@ Errors:
RefreshToken:
Invalid: Refresh Token ist ungültig
NotFound: Refresh Token nicht gefunden
+ Instance:
+ NotFound: Instanz konnte nicht gefunden werden
+ AlreadyExists: Instanz exisitiert bereits
+ NotChanged: Instanz wurde nicht verändert
Org:
AlreadyExists: Organisationsname existiert bereits
Invalid: Organisation ist ungültig
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index bca370719e..bcd225db78 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -142,6 +142,10 @@ Errors:
RefreshToken:
Invalid: Refresh Token is invalid
NotFound: Refresh Token not found
+ Instance:
+ NotFound: Instance not found
+ AlreadyExists: Instance already exists
+ NotChanged: Instance not changed
Org:
AlreadyExists: Organisation's name already taken
Invalid: Organisation is invalid
diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml
index cdb0a61d03..aba621bea4 100644
--- a/internal/static/i18n/fr.yaml
+++ b/internal/static/i18n/fr.yaml
@@ -142,6 +142,10 @@ Errors:
RefreshToken:
Invalid: Le jeton de rafraîchissement n'est pas valide
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:
AlreadyExists: Le nom de l'organisation est déjà pris
Invalid: L'organisation n'est pas valide
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index cfbddcd88c..09d010862b 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -142,6 +142,10 @@ Errors:
RefreshToken:
Invalid: Refresh Token non è valido
NotFound: Refresh Token non trovato
+ Instance:
+ NotFound: Istanza non trovata
+ AlreadyExists: L'istanza esiste già
+ NotChanged: Istanza non modificata
Org:
AlreadyExists: Nome dell'organizzazione già preso
Invalid: L'organizzazione non è valida
diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml
index 9d145da32e..3288e1cec1 100644
--- a/internal/static/i18n/zh.yaml
+++ b/internal/static/i18n/zh.yaml
@@ -142,6 +142,10 @@ Errors:
RefreshToken:
Invalid: Refresh Token 无效
NotFound: 未找到 Refresh Token
+ Instance:
+ NotFound: 没有找到实例
+ AlreadyExists: 实例已经存在
+ NotChanged: 实例没有改变
Org:
AlreadyExists: 组织名称已被占用
Invalid: 组织无效
diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto
index 2d9e47a279..f9b7582a69 100644
--- a/proto/zitadel/system.proto
+++ b/proto/zitadel/system.proto
@@ -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
// This might take some time
rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) {
@@ -397,6 +409,15 @@ message AddInstanceResponse {
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 {
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}