diff --git a/apps/iam/kinds/user.cue b/apps/iam/kinds/user.cue index fc5bb8fb153..602500943ee 100644 --- a/apps/iam/kinds/user.cue +++ b/apps/iam/kinds/user.cue @@ -33,19 +33,18 @@ userv0alpha1: userKind & { lastSeenAt: int64 | 0 } } - // TODO: Uncomment when the custom routes implementation is done - // routes: { - // "/teams": { - // "GET": { - // response: { - // #UserTeam: { - // title: string - // teamRef: v0alpha1.TeamRef - // permission: v0alpha1.TeamPermission - // } - // items: [...#UserTeam] - // } - // } - // } - // } + routes: { + "/teams": { + "GET": { + response: { + #UserTeam: { + title: string + teamRef: v0alpha1.TeamRef + permission: v0alpha1.TeamPermission + } + items: [...#UserTeam] + } + } + } + } } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go index bd7af9b3361..390f2acfce4 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go @@ -2,6 +2,9 @@ package v0alpha1 import ( "context" + "encoding/json" + "fmt" + "net/http" "github.com/grafana/grafana-app-sdk/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -97,3 +100,24 @@ func (c *UserClient) UpdateStatus(ctx context.Context, identifier resource.Ident func (c *UserClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } + +type GetTeamsRequest struct { + Headers http.Header +} + +func (c *UserClient) GetTeams(ctx context.Context, identifier resource.Identifier, request GetTeamsRequest) (*GetTeams, error) { + resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{ + Path: "/teams", + Verb: "GET", + Headers: request.Headers, + }) + if err != nil { + return nil, err + } + cast := GetTeams{} + err = json.Unmarshal(resp, &cast) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response bytes into GetTeams: %w", err) + } + return &cast, nil +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_getteams_response_body_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_getteams_response_body_types_gen.go new file mode 100644 index 00000000000..56504c6abb9 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_getteams_response_body_types_gen.go @@ -0,0 +1,48 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam struct { + Title string `json:"title"` + TeamRef TeamRef `json:"teamRef"` + Permission TeamPermission `json:"permission"` +} + +// NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam creates a new VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam object. +func NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam() *VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam { + return &VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{ + TeamRef: *NewTeamRef(), + } +} + +// +k8s:openapi-gen=true +type TeamRef struct { + // Name is the unique identifier for a team. + Name string `json:"name"` +} + +// NewTeamRef creates a new TeamRef object. +func NewTeamRef() *TeamRef { + return &TeamRef{} +} + +// +k8s:openapi-gen=true +type TeamPermission string + +const ( + TeamPermissionAdmin TeamPermission = "admin" + TeamPermissionMember TeamPermission = "member" +) + +// +k8s:openapi-gen=true +type GetTeamsBody struct { + Items []VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam `json:"items"` +} + +// NewGetTeamsBody creates a new GetTeamsBody object. +func NewGetTeamsBody() *GetTeamsBody { + return &GetTeamsBody{ + Items: []VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_getteams_response_object_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_getteams_response_object_types_gen.go new file mode 100644 index 00000000000..0554fe7e6bc --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_getteams_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetTeams struct { + metav1.TypeMeta `json:",inline"` + GetTeamsBody `json:",inline"` +} + +func NewGetTeams() *GetTeams { + return &GetTeams{} +} + +func (t *GetTeamsBody) DeepCopyInto(dst *GetTeamsBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetTeams) DeepCopyObject() runtime.Object { + dst := NewGetTeams() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetTeams) DeepCopyInto(dst *GetTeams) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetTeamsBody.DeepCopyInto(&dst.GetTeamsBody) +} + +var _ runtime.Object = NewGetTeams() diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index 87128d27699..9255a733b28 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -22,6 +22,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeams": schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeamsBody": schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchUsers": schema_pkg_apis_iam_v0alpha1_GetSearchUsers(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeams": schema_pkg_apis_iam_v0alpha1_GetTeams(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeamsBody": schema_pkg_apis_iam_v0alpha1_GetTeamsBody(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref), @@ -69,6 +71,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef": schema_pkg_apis_iam_v0alpha1_TeamRef(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref), @@ -77,6 +80,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref), } @@ -745,6 +749,76 @@ func schema_pkg_apis_iam_v0alpha1_GetSearchUsers(ref common.ReferenceCallback) c } } +func schema_pkg_apis_iam_v0alpha1_GetTeams(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_GetTeamsBody(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam"}, + } +} + func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2721,6 +2795,27 @@ func schema_pkg_apis_iam_v0alpha1_TeamList(ref common.ReferenceCallback) common. } } +func schema_pkg_apis_iam_v0alpha1_TeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the unique identifier for a team.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + func schema_pkg_apis_iam_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -3118,6 +3213,41 @@ func schema_pkg_apis_iam_v0alpha1_UserStatus(ref common.ReferenceCallback) commo } } +func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "teamRef": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef"), + }, + }, + "permission": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"title", "teamRef", "permission"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef"}, + } +} + func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 664c12226de..5ee9ddfa954 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -74,6 +74,65 @@ var appManifestData = app.ManifestData{ Plural: "Users", Scope: "Namespaced", Conversion: false, + Routes: map[string]spec3.PathProps{ + "/teams": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getTeams", + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getTeamsUserTeam"), + }}, + }, + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + }, + Required: []string{ + "items", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, }, { @@ -544,6 +603,8 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist } var customRouteToGoResponseType = map[string]any{ + "v0alpha1|User|teams|GET": v0alpha1.GetTeams{}, + "v0alpha1|Team|groups|GET": v0alpha1.GetGroups{}, "v0alpha1||/searchTeams|GET": v0alpha1.GetSearchTeams{}, diff --git a/apps/iam/pkg/app/app.go b/apps/iam/pkg/app/app.go index ff2a0cc7feb..0ad9582bffb 100644 --- a/apps/iam/pkg/app/app.go +++ b/apps/iam/pkg/app/app.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/apps/iam/pkg/reconcilers" "github.com/grafana/grafana/pkg/services/authz" ) @@ -101,6 +102,17 @@ func New(cfg app.Config) (app.App, error) { }, }, }, + ManagedKinds: []simple.AppManagedKind{ + { + Kind: v0alpha1.UserKind(), + CustomRoutes: simple.AppCustomRouteHandlers{ + { + Path: "teams", + Method: "GET", + }: GetTeamsHandler, + }, + }, + }, UnmanagedKinds: []simple.AppUnmanagedKind{ { Kind: foldersKind.FolderKind(), diff --git a/apps/iam/pkg/app/routes.go b/apps/iam/pkg/app/routes.go new file mode 100644 index 00000000000..bcb35637565 --- /dev/null +++ b/apps/iam/pkg/app/routes.go @@ -0,0 +1,27 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" +) + +// GetTeamsHandler handles requests for the GET /teams subresource route on User +func GetTeamsHandler(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + // Dummy implementation - returns empty list + response := v0alpha1.GetTeams{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v0alpha1.APIGroup, v0alpha1.APIVersion), + Kind: "GetTeams", + }, + GetTeamsBody: v0alpha1.GetTeamsBody{ + Items: []v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{}, + }, + } + return json.NewEncoder(writer).Encode(response) +} diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index f03b3c3369b..2080f78aa4d 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -6776,6 +6776,42 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeams": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetTeamsBody": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "default": {} + } + } + } + }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": { "type": "object", "required": [ @@ -7890,6 +7926,19 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name is the unique identifier for a team.", + "type": "string", + "default": "" + } + } + }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": { "type": "object", "required": [ @@ -8134,6 +8183,27 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam": { + "type": "object", + "required": [ + "title", + "teamRef", + "permission" + ], + "properties": { + "permission": { + "type": "string", + "default": "" + }, + "teamRef": { + "default": {} + }, + "title": { + "type": "string", + "default": "" + } + } + }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": { "type": "object", "required": [