diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index a59b674b78..ae98bcfa5e 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -63,6 +63,8 @@ func InstanceQueryToModel(searchQuery *instance_pb.Query) (query.SearchQuery, er switch q := searchQuery.Query.(type) { case *instance_pb.Query_IdQuery: return query.NewInstanceIDsListSearchQuery(q.IdQuery.Ids...) + case *instance_pb.Query_DomainQuery: + return query.NewInstanceDomainsListSearchQuery(q.DomainQuery.Domains...) default: return nil, errors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid") } diff --git a/internal/api/grpc/system/instance_integration_test.go b/internal/api/grpc/system/instance_integration_test.go new file mode 100644 index 0000000000..f874ac79f0 --- /dev/null +++ b/internal/api/grpc/system/instance_integration_test.go @@ -0,0 +1,109 @@ +//go:build integration + +package system_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/pkg/grpc/instance" + "github.com/zitadel/zitadel/pkg/grpc/object" + system_pb "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func TestServer_ListInstances(t *testing.T) { + domain, instanceID, _ := Tester.UseIsolatedInstance(CTX, SystemCTX) + + tests := []struct { + name string + req *system_pb.ListInstancesRequest + want []*instance.Instance + wantErr bool + }{ + { + name: "empty query error", + req: &system_pb.ListInstancesRequest{ + Queries: []*instance.Query{{}}, + }, + wantErr: true, + }, + { + name: "non-existing id", + req: &system_pb.ListInstancesRequest{ + Queries: []*instance.Query{{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{"foo"}, + }, + }, + }}, + }, + want: []*instance.Instance{}, + }, + { + name: "get 1 by id", + req: &system_pb.ListInstancesRequest{ + Query: &object.ListQuery{ + Limit: 1, + }, + Queries: []*instance.Query{{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{instanceID}, + }, + }, + }}, + }, + want: []*instance.Instance{{ + Id: instanceID, + }}, + }, + { + name: "non-existing domain", + req: &system_pb.ListInstancesRequest{ + Queries: []*instance.Query{{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{"foo"}, + }, + }, + }}, + }, + want: []*instance.Instance{}, + }, + { + name: "get 1 by domain", + req: &system_pb.ListInstancesRequest{ + Query: &object.ListQuery{ + Limit: 1, + }, + Queries: []*instance.Query{{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{domain}, + }, + }, + }}, + }, + want: []*instance.Instance{{ + Id: instanceID, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Tester.Client.System.ListInstances(SystemCTX, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + got := resp.GetResult() + assert.Len(t, got, len(tt.want)) + for i := 0; i < len(tt.want); i++ { + assert.Equalf(t, tt.want[i].GetId(), got[i].GetId(), "instance[%d] id", i) + } + }) + } +} diff --git a/internal/query/instance.go b/internal/query/instance.go index f570f05912..c80ae9085b 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -150,6 +150,15 @@ func NewInstanceIDsListSearchQuery(ids ...string) (SearchQuery, error) { return NewListQuery(InstanceColumnID, list, ListIn) } +func NewInstanceDomainsListSearchQuery(domains ...string) (SearchQuery, error) { + list := make([]interface{}, len(domains)) + for i, value := range domains { + list[i] = value + } + + return NewListQuery(InstanceDomainDomainCol, list, ListIn) +} + func (q *InstanceSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { @@ -280,7 +289,8 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu return sq.Select( InstanceColumnID.identifier(), countColumn.identifier(), - ).From(instanceTable.identifier()), + ).From(instanceTable.identifier()). + LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)), func(builder sq.SelectBuilder) sq.SelectBuilder { return sq.Select( instanceFilterCountColumn, diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index a14b44df0d..27e206e0ee 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -55,7 +55,8 @@ var ( ` projections.instance_domains.creation_date,` + ` projections.instance_domains.change_date, ` + ` projections.instance_domains.sequence` + - ` FROM (SELECT projections.instances.id, COUNT(*) OVER () FROM projections.instances) AS f` + + ` FROM (SELECT projections.instances.id, COUNT(*) OVER () FROM projections.instances` + + ` LEFT JOIN projections.instance_domains ON projections.instances.id = projections.instance_domains.instance_id) AS f` + ` LEFT JOIN projections.instances ON f.id = projections.instances.id` + ` LEFT JOIN projections.instance_domains ON f.id = projections.instance_domains.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index 88b2d269f0..e6fdbd3411 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -71,6 +71,7 @@ message Query { option (validate.required) = true; IdsQuery id_query = 1; + DomainsQuery domain_query = 2; } } @@ -83,6 +84,17 @@ message IdsQuery { ]; } +message DomainsQuery { + repeated string domains = 1 [ + (validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_items: 20, + example: "[\"my-instace.zitadel.cloud\", \"auth.custom.com\"]"; + description: "Return the instances that have the requested domains"; + } + ]; +} + enum FieldName { FIELD_NAME_UNSPECIFIED = 0; FIELD_NAME_ID = 1;