From b7f4efd7bc7491a44bbac943862cf4ebbb34703d Mon Sep 17 00:00:00 2001 From: Antoine CORDIER Date: Wed, 22 Jan 2025 16:17:54 +0000 Subject: [PATCH] feat: implement controller for groups see https://gravitee.atlassian.net/browse/GKO-938 --- .circleci/config.yml | 1 + api/model/api/base/api.go | 9 +- api/model/api/base/member.go | 18 +- api/model/api/base/zz_generated.deepcopy.go | 5 + api/model/api/v2/api.go | 16 ++ api/model/api/v4/api.go | 16 ++ api/model/group/group.go | 76 +++++++++ api/model/group/zz_generated.deepcopy.go | 83 ++++++++++ api/v1alpha1/apiv2definition_types.go | 16 ++ api/v1alpha1/apiv4definition_types.go | 12 ++ api/v1alpha1/group_types.go | 156 ++++++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 101 ++++++++++++ .../apim/apidefinition/internal/groups.go | 64 +++++++ .../apim/apidefinition/internal/update.go | 8 + controllers/apim/group/group_controller.go | 112 +++++++++++++ controllers/apim/group/internal/delete.go | 39 +++++ controllers/apim/group/internal/status.go | 37 +++++ controllers/apim/group/internal/update.go | 46 ++++++ docs/api/reference.md | 108 +++++++++++- examples/apim/group/group.yml | 31 ++++ helm/gko/crds/gravitee.io_apidefinitions.yaml | 21 ++- .../crds/gravitee.io_apiv4definitions.yaml | 21 ++- helm/gko/crds/gravitee.io_groups.yaml | 151 +++++++++++++++++ .../templates/rbac/manager-cluster-role.yaml | 27 +++ helm/gko/templates/rbac/manager-role.yaml | 27 +++ .../rbac/resource-patch-cluster-role.yaml | 1 + .../templates/webhook/mutation-webhook.yaml | 27 +++ .../templates/webhook/validation-webhook.yaml | 27 +++ internal/admission/group/crtl.go | 60 +++++++ internal/admission/group/validate.go | 67 ++++++++ internal/apim/model/env.go | 4 + internal/apim/service/env.go | 27 +++ internal/core/interface.go | 4 + internal/core/keys.go | 1 + internal/log/log.go | 29 +++- internal/predicate/predicate.go | 5 + main.go | 15 +- .../integration/admission/group/suite_test.go | 43 +++++ ...update_withContext_andPrimaryOwner_test.go | 86 ++++++++++ .../v2/create_withContext_andGroupRef_test.go | 87 ++++++++++ .../v4/create_withContext_andGroupRef_test.go | 87 ++++++++++ test/internal/integration/assert/assert.go | 8 + .../integration/constants/constants.go | 2 + test/internal/integration/fixture/apply.go | 18 ++ test/internal/integration/fixture/build.go | 20 +++ test/internal/integration/fixture/decode.go | 1 + test/internal/integration/fixture/objects.go | 1 + test/internal/integration/manager/manager.go | 18 +- 48 files changed, 1814 insertions(+), 25 deletions(-) create mode 100644 api/model/group/group.go create mode 100644 api/model/group/zz_generated.deepcopy.go create mode 100644 api/v1alpha1/group_types.go create mode 100644 controllers/apim/apidefinition/internal/groups.go create mode 100644 controllers/apim/group/group_controller.go create mode 100644 controllers/apim/group/internal/delete.go create mode 100644 controllers/apim/group/internal/status.go create mode 100644 controllers/apim/group/internal/update.go create mode 100644 examples/apim/group/group.yml create mode 100644 helm/gko/crds/gravitee.io_groups.yaml create mode 100644 internal/admission/group/crtl.go create mode 100644 internal/admission/group/validate.go create mode 100644 test/integration/admission/group/suite_test.go create mode 100644 test/integration/admission/group/update_withContext_andPrimaryOwner_test.go create mode 100644 test/integration/apidefinition/v2/create_withContext_andGroupRef_test.go create mode 100644 test/integration/apidefinition/v4/create_withContext_andGroupRef_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 1912da933..68fd509d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -218,6 +218,7 @@ jobs: name: Run IT with filter << parameters.ginkgo-filter >> command: | export SAFE_RANDOM=true + export GKO_MANAGER_SILENT_LOG=true IT_ARGS="--label-filter=<< parameters.ginkgo-filter >> --flake-attempts=2 --cover --coverprofile=cover-<< parameters.ginkgo-filter >>.out --coverpkg=github.com/gravitee-io/gravitee-kubernetes-operator/... --output-dir=/tmp/junit/reports" make -s it - store_test_results: path: /tmp/junit/reports diff --git a/api/model/api/base/api.go b/api/model/api/base/api.go index a5c11d928..25993ab5e 100644 --- a/api/model/api/base/api.go +++ b/api/model/api/base/api.go @@ -16,6 +16,7 @@ package base import ( + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" ) @@ -60,10 +61,16 @@ type ApiBase struct { // of an existing API resource definition. // +kubebuilder:default:={} Resources []*ResourceOrRef `json:"resources"` - // List of groups associated with the API + // List of groups associated with the API. + // This groups are id or name references to existing groups in APIM. // +kubebuilder:validation:Optional // +kubebuilder:default:={} Groups []string `json:"groups"` + // List of group references associated with the API + // This groups are references to Group custom resources created on the cluster. + // +kubebuilder:validation:Optional + // +kubebuilder:default:={} + GroupRefs []refs.NamespacedName `json:"groupRefs"` // +kubebuilder:validation:Optional // The list of categories the API belongs to. // Categories are reflected in APIM portal so that consumers can easily find the APIs they need. diff --git a/api/model/api/base/member.go b/api/model/api/base/member.go index bd09c83bb..b881d6876 100644 --- a/api/model/api/base/member.go +++ b/api/model/api/base/member.go @@ -14,7 +14,11 @@ package base -import "fmt" +import ( + "fmt" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" +) type Member struct { // Member source @@ -52,3 +56,15 @@ func NewMemoryMember(username, role string) *Member { Role: role, } } + +func NewGraviteeGroupMember(username, role string) group.Member { + return group.Member{ + Source: "gravitee", + SourceID: username, + Roles: map[group.RoleScope]string{ + group.APIRoleScope: role, + group.ApplicationRoleScope: role, + group.IntegrationRoleScope: role, + }, + } +} diff --git a/api/model/api/base/zz_generated.deepcopy.go b/api/model/api/base/zz_generated.deepcopy.go index 2287117f6..e1de40139 100644 --- a/api/model/api/base/zz_generated.deepcopy.go +++ b/api/model/api/base/zz_generated.deepcopy.go @@ -90,6 +90,11 @@ func (in *ApiBase) DeepCopyInto(out *ApiBase) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.GroupRefs != nil { + in, out := &in.GroupRefs, &out.GroupRefs + *out = make([]refs.NamespacedName, len(*in)) + copy(*out, *in) + } if in.Categories != nil { in, out := &in.Categories, &out.Categories *out = make([]string, len(*in)) diff --git a/api/model/api/v2/api.go b/api/model/api/v2/api.go index b9d1f1392..7809100b1 100644 --- a/api/model/api/v2/api.go +++ b/api/model/api/v2/api.go @@ -154,6 +154,22 @@ func (api *Api) SetDefinitionContext(ctx core.DefinitionContext) { } } +func (api *Api) GetGroupRefs() []core.ObjectRef { + refs := make([]core.ObjectRef, 0) + for i := range api.GroupRefs { + refs = append(refs, &api.GroupRefs[i]) + } + return refs +} + +func (api *Api) GetGroups() []string { + return api.Groups +} + +func (api *Api) SetGroups(groups []string) { + api.Groups = groups +} + func (ctx *DefinitionContext) GetOrigin() string { if ctx == nil { return OriginKubernetes diff --git a/api/model/api/v4/api.go b/api/model/api/v4/api.go index 93663f318..5b9229671 100644 --- a/api/model/api/v4/api.go +++ b/api/model/api/v4/api.go @@ -97,6 +97,22 @@ func (api *Api) GetType() string { return string(api.Type) } +func (api *Api) GetGroupRefs() []core.ObjectRef { + refs := make([]core.ObjectRef, 0) + for i := range api.GroupRefs { + refs = append(refs, &api.GroupRefs[i]) + } + return refs +} + +func (api *Api) GetGroups() []string { + return api.Groups +} + +func (api *Api) SetGroups(groups []string) { + api.Groups = groups +} + type GatewayDefinitionApi struct { *Api `json:",inline"` Version string `json:"apiVersion"` diff --git a/api/model/group/group.go b/api/model/group/group.go new file mode 100644 index 000000000..1558f1b5c --- /dev/null +++ b/api/model/group/group.go @@ -0,0 +1,76 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +kubebuilder:object:generate=true +package group + +import ( + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/status" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" +) + +// +kubebuilder:validation:Enum=API;APPLICATION;INTEGRATION; +type RoleScope string + +const ( + APIRoleScope = RoleScope("API") + ApplicationRoleScope = RoleScope("APPLICATION") + IntegrationRoleScope = RoleScope("INTEGRATION") +) + +type Type struct { + // +kubebuilder:validation:Optional + ID string `json:"id,omitempty"` + // +kubebuilder:validation:Required + Name string `json:"name"` + // +kubebuilder:validation:Optional + // +kubebuilder:default:=true + // If true, new members added to the API spec will + // be notified when the API is synced with APIM. + NotifyMembers bool `json:"notifyMembers"` + Members []Member `json:"members"` +} + +type Member struct { + // Member source + // +kubebuilder:validation:Required + // +kubebuilder:example:=gravitee + Source string `json:"source"` + // Member source ID + // +kubebuilder:validation:Required + // +kubebuilder:example:=user@email.com + SourceID string `json:"sourceId"` + // +kubebuilder:validation:Optional + // +kubebuilder:default:={} + Roles map[RoleScope]string `json:"roles"` +} + +type Status struct { + // The ID of the Group in the Gravitee API Management instance + // +kubebuilder:validation:Optional + ID string `json:"id,omitempty"` + // The organization ID defined in the management context + // +kubebuilder:validation:Optional + OrgID string `json:"organizationId,omitempty"` + // The environment ID defined in the management context + // +kubebuilder:validation:Optional + EnvID string `json:"environmentId,omitempty"` + // The processing status of the Group. + ProcessingStatus core.ProcessingStatus `json:"processingStatus,omitempty"` + // The number of members added to this group + Members uint `json:"members"` + // When group has been created regardless of errors, this field is + // used to persist the error message encountered during admission + Errors status.Errors `json:"errors,omitempty"` +} diff --git a/api/model/group/zz_generated.deepcopy.go b/api/model/group/zz_generated.deepcopy.go new file mode 100644 index 000000000..17c1ea36a --- /dev/null +++ b/api/model/group/zz_generated.deepcopy.go @@ -0,0 +1,83 @@ +//go:build !ignore_autogenerated + +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by controller-gen. DO NOT EDIT. + +package group + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Member) DeepCopyInto(out *Member) { + *out = *in + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make(map[RoleScope]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Member. +func (in *Member) DeepCopy() *Member { + if in == nil { + return nil + } + out := new(Member) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in + in.Errors.DeepCopyInto(&out.Errors) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Type) DeepCopyInto(out *Type) { + *out = *in + if in.Members != nil { + in, out := &in.Members, &out.Members + *out = make([]Member, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Type. +func (in *Type) DeepCopy() *Type { + if in == nil { + return nil + } + out := new(Type) + in.DeepCopyInto(out) + return out +} diff --git a/api/v1alpha1/apiv2definition_types.go b/api/v1alpha1/apiv2definition_types.go index 50b36a58d..f1f9093c9 100644 --- a/api/v1alpha1/apiv2definition_types.go +++ b/api/v1alpha1/apiv2definition_types.go @@ -170,6 +170,18 @@ func (api *ApiDefinition) GetPlan(name string) core.PlanModel { return api.Spec.GetPlan(name) } +func (api *ApiDefinition) GetGroupRefs() []core.ObjectRef { + return api.Spec.GetGroupRefs() +} + +func (api *ApiDefinition) GetGroups() []string { + return api.Spec.Groups +} + +func (api *ApiDefinition) SetGroups(groups []string) { + api.Spec.Groups = groups +} + func (api *ApiDefinition) GetRef() core.ObjectRef { return &refs.NamespacedName{ Name: api.Name, @@ -177,6 +189,10 @@ func (api *ApiDefinition) GetRef() core.ObjectRef { } } +func (api *ApiDefinition) IsBeingDeleted() bool { + return !api.ObjectMeta.DeletionTimestamp.IsZero() +} + func (api *ApiDefinition) PopulateIDs(_ core.ContextModel) { api.Spec.ID = api.pickID() api.Spec.CrossID = api.pickCrossID() diff --git a/api/v1alpha1/apiv4definition_types.go b/api/v1alpha1/apiv4definition_types.go index 450460588..0c5bc2871 100644 --- a/api/v1alpha1/apiv4definition_types.go +++ b/api/v1alpha1/apiv4definition_types.go @@ -323,6 +323,18 @@ func getFLowSharedPolicyGroupsReferences(flows []*v4.Flow) []*refs.NamespacedNam return results } +func (api *ApiV4Definition) GetGroupRefs() []core.ObjectRef { + return api.Spec.GetGroupRefs() +} + +func (api *ApiV4Definition) GetGroups() []string { + return api.Spec.Groups +} + +func (api *ApiV4Definition) SetGroups(groups []string) { + api.Spec.Groups = groups +} + func (spec *ApiV4DefinitionSpec) Hash() string { return hash.Calculate(spec) } diff --git a/api/v1alpha1/group_types.go b/api/v1alpha1/group_types.go new file mode 100644 index 000000000..7a52aec6f --- /dev/null +++ b/api/v1alpha1/group_types.go @@ -0,0 +1,156 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + "fmt" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/hash" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/uuid" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ core.ContextAwareObject = &Group{} +var _ core.Spec = &GroupSpec{} +var _ core.Status = &GroupStatus{} + +// +kubebuilder:object:generate=true +type GroupSpec struct { + *group.Type `json:",inline"` + Context *refs.NamespacedName `json:"contextRef,omitempty"` +} + +func (spec *GroupSpec) Hash() string { + return hash.Calculate(spec) +} + +type GroupStatus struct { + group.Status `json:",inline"` +} + +func (s *GroupStatus) DeepCopyFrom(obj client.Object) error { + switch t := obj.(type) { + case *Group: + t.Status.DeepCopyInto(s) + default: + return fmt.Errorf("unknown type %T", t) + } + + return nil +} + +func (s *GroupStatus) DeepCopyTo(obj client.Object) error { + switch t := obj.(type) { + case *Group: + s.DeepCopyInto(&t.Status) + default: + return fmt.Errorf("unknown type %T", t) + } + + return nil +} + +func (s *GroupStatus) IsFailed() bool { + return s.ProcessingStatus == core.ProcessingStatusFailed +} + +func (s *GroupStatus) SetProcessingStatus(status core.ProcessingStatus) { + s.ProcessingStatus = status +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Members at",type=string,JSONPath=`.status.members`,description="The number of members added to the group" +// +kubebuilder:storageversion +type Group struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec GroupSpec `json:"spec,omitempty"` + Status GroupStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type GroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Group `json:"items"` +} + +func (g *Group) GetRef() core.ObjectRef { + return &refs.NamespacedName{ + Name: g.Name, + Namespace: g.Namespace, + } +} + +func (g *Group) GetSpec() core.Spec { + return &g.Spec +} + +func (g *Group) GetStatus() core.Status { + return &g.Status +} + +func (g *Group) IsBeingDeleted() bool { + return !g.ObjectMeta.DeletionTimestamp.IsZero() +} + +func (g *Group) HasContext() bool { + return g.Spec.Context != nil +} + +func (g *Group) ContextRef() core.ObjectRef { + return g.Spec.Context +} + +func (g *Group) GetEnvID() string { + return g.Status.EnvID +} + +func (g *Group) GetID() string { + return g.Status.ID +} + +func (g *Group) GetOrgID() string { + return g.Status.OrgID +} + +func (g *Group) PopulateIDs(mCtx core.ContextModel) { + g.Spec.ID = g.pickID(mCtx) +} + +func (g *Group) pickID(mCtx core.ContextModel) string { + if g.Status.ID != "" { + return g.Status.ID + } + + if g.Spec.ID != "" { + return g.Spec.ID + } + + if mCtx != nil { + return uuid.FromStrings(g.Spec.Name, mCtx.GetOrgID(), mCtx.GetEnvID()) + } + + return string(g.UID) +} + +func init() { + SchemeBuilder.Register(&Group{}, &GroupList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 446fac901..acb215159 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package v1alpha1 import ( "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/policygroups" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" @@ -410,6 +411,106 @@ func (in *ApplicationStatus) DeepCopy() *ApplicationStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Group) DeepCopyInto(out *Group) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. +func (in *Group) DeepCopy() *Group { + if in == nil { + return nil + } + out := new(Group) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Group) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupList) DeepCopyInto(out *GroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Group, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupList. +func (in *GroupList) DeepCopy() *GroupList { + if in == nil { + return nil + } + out := new(GroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupSpec) DeepCopyInto(out *GroupSpec) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(group.Type) + (*in).DeepCopyInto(*out) + } + if in.Context != nil { + in, out := &in.Context, &out.Context + *out = new(refs.NamespacedName) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupSpec. +func (in *GroupSpec) DeepCopy() *GroupSpec { + if in == nil { + return nil + } + out := new(GroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupStatus) DeepCopyInto(out *GroupStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupStatus. +func (in *GroupStatus) DeepCopy() *GroupStatus { + if in == nil { + return nil + } + out := new(GroupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagementContext) DeepCopyInto(out *ManagementContext) { *out = *in diff --git a/controllers/apim/apidefinition/internal/groups.go b/controllers/apim/apidefinition/internal/groups.go new file mode 100644 index 000000000..4f37109b9 --- /dev/null +++ b/controllers/apim/apidefinition/internal/groups.go @@ -0,0 +1,64 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + "reflect" + "slices" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/log" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ResolveGroupRefs(ctx context.Context, api core.ApiDefinitionObject) error { + groupRefs := api.GetGroupRefs() + + if groupRefs == nil || reflect.ValueOf(groupRefs).IsNil() { + return nil + } + + groups := api.GetGroups() + for _, ref := range groupRefs { + group := new(v1alpha1.Group) + nsn := getNamespacedName(ref, api.GetNamespace()) + err := k8s.GetClient().Get(ctx, nsn, group) + if client.IgnoreNotFound(err) != nil { + return err + } else if err != nil { + log.Debug(ctx, "Skipping group reference "+ref.String()+" as it does not exist") + continue + } + if !slices.Contains(groups, group.Spec.Name) { + groups = append(groups, group.Spec.Name) + } + } + api.SetGroups(groups) + return nil +} + +func getNamespacedName(ref core.ObjectRef, apiNs string) types.NamespacedName { + if ref.GetNamespace() == "" { + return types.NamespacedName{ + Name: ref.GetName(), + Namespace: apiNs, + } + } + return ref.NamespacedName() +} diff --git a/controllers/apim/apidefinition/internal/update.go b/controllers/apim/apidefinition/internal/update.go index 60b204420..8617a96d5 100644 --- a/controllers/apim/apidefinition/internal/update.go +++ b/controllers/apim/apidefinition/internal/update.go @@ -53,6 +53,10 @@ func createOrUpdateV2(ctx context.Context, apiDefinition *v1alpha1.ApiDefinition return err } + if err := ResolveGroupRefs(ctx, cp); err != nil { + return err + } + cp.PopulateIDs(nil) if !apiDefinition.HasContext() { @@ -113,6 +117,10 @@ func createOrUpdateV4(ctx context.Context, apiDefinition *v1alpha1.ApiV4Definiti return err } + if err := ResolveGroupRefs(ctx, cp); err != nil { + return err + } + spec.DefinitionContext = v4.NewDefaultKubernetesContext().MergeWith(spec.DefinitionContext) if spec.Context != nil { diff --git a/controllers/apim/group/group_controller.go b/controllers/apim/group/group_controller.go new file mode 100644 index 000000000..0fd64b530 --- /dev/null +++ b/controllers/apim/group/group_controller.go @@ -0,0 +1,112 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "context" + "time" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/group/internal" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/event" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/hash" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/log" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/predicate" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/template" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const requeueAfterTime = time.Second * 5 + +type Reconciler struct { + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + group := &v1alpha1.Group{} + if err := k8s.GetClient().Get(ctx, req.NamespacedName, group); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + group.SetNamespace(req.Namespace) + + events := event.NewRecorder(r.Recorder) + + dc := group.DeepCopy() + + _, err := util.CreateOrUpdate(ctx, k8s.GetClient(), group, func() error { + util.AddFinalizer(group, core.GroupFinalizer) + k8s.AddAnnotation(group, core.LastSpecHashAnnotation, hash.Calculate(&dc.Spec)) + + if err := template.Compile(ctx, dc); err != nil { + group.Status.ProcessingStatus = core.ProcessingStatusFailed + return err + } + + var err error + if group.IsBeingDeleted() { + err = events.Record(event.Delete, group, func() error { + if err := internal.Delete(ctx, dc); err != nil { + return err + } + util.RemoveFinalizer(group, core.GroupFinalizer) + return nil + }) + } else { + err = events.Record(event.Update, group, func() error { + return internal.CreateOrUpdate(ctx, dc) + }) + } + + return err + }) + + if err := dc.GetStatus().DeepCopyTo(group); err != nil { + return ctrl.Result{}, err + } + + if err == nil { + log.InfoEndReconcile(ctx, group) + return ctrl.Result{}, internal.UpdateStatusSuccess(ctx, group) + } + + // An error occurred during the reconcile + if err := internal.UpdateStatusFailure(ctx, group); err != nil { + return ctrl.Result{}, err + } + + if errors.IsRecoverable(err) { + log.ErrorRequeuingReconcile(ctx, err, group) + return ctrl.Result{RequeueAfter: requeueAfterTime}, err + } + + log.ErrorAbortingReconcile(ctx, err, group) + return ctrl.Result{}, nil +} + +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Group{}). + WithEventFilter(predicate.LastSpecHashPredicate{}). + Complete(r) +} diff --git a/controllers/apim/group/internal/delete.go b/controllers/apim/group/internal/delete.go new file mode 100644 index 000000000..519829b73 --- /dev/null +++ b/controllers/apim/group/internal/delete.go @@ -0,0 +1,39 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" + util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func Delete(ctx context.Context, group *v1alpha1.Group) error { + if !util.ContainsFinalizer(group, core.GroupFinalizer) { + return nil + } + + ns := group.Namespace + + apim, err := apim.FromContextRef(ctx, group.ContextRef(), ns) + if err != nil { + return err + } + + return apim.Env.DeleteGroup(group.Status.ID) +} diff --git a/controllers/apim/group/internal/status.go b/controllers/apim/group/internal/status.go new file mode 100644 index 000000000..10702e0ee --- /dev/null +++ b/controllers/apim/group/internal/status.go @@ -0,0 +1,37 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" +) + +func UpdateStatusSuccess(ctx context.Context, group *v1alpha1.Group) error { + if group.IsBeingDeleted() { + return nil + } + + group.Status.ProcessingStatus = core.ProcessingStatusCompleted + return k8s.GetClient().Status().Update(ctx, group) +} + +func UpdateStatusFailure(ctx context.Context, group *v1alpha1.Group) error { + group.Status.ProcessingStatus = core.ProcessingStatusFailed + return k8s.GetClient().Status().Update(ctx, group) +} diff --git a/controllers/apim/group/internal/update.go b/controllers/apim/group/internal/update.go new file mode 100644 index 000000000..bcb6db593 --- /dev/null +++ b/controllers/apim/group/internal/update.go @@ -0,0 +1,46 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim" +) + +func CreateOrUpdate(ctx context.Context, group *v1alpha1.Group) error { + ns := group.Namespace + spec := group.Spec + + apim, err := apim.FromContextRef(ctx, group.ContextRef(), ns) + if err != nil { + return err + } + + group.PopulateIDs(apim.Context) + + status, err := apim.Env.ImportGroup(spec.Type) + if err != nil { + return err + } + + group.Status.ID = status.ID + group.Status.OrgID = apim.Context.GetOrgID() + group.Status.EnvID = apim.Context.GetEnvID() + group.Status.Members = status.Members + + return nil +} diff --git a/docs/api/reference.md b/docs/api/reference.md index 7e3e9adaa..26375aa62 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -535,11 +535,22 @@ is managed using a kubernetes operator
Default: 2.0.0
false + + groupRefs + []object + + List of group references associated with the API +This groups are references to Group custom resources created on the cluster.
+
+ Default: []
+ + false groups []string - List of groups associated with the API
+ List of groups associated with the API. +This groups are id or name references to existing groups in APIM.

Default: []
@@ -1098,6 +1109,47 @@ List of path operators +### ApiDefinition.spec.groupRefs[index] +[Go to parent definition](#apidefinitionspec) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring +
+
true
kindstring +
+
false
namespacestring +
+
false
+ + ### ApiDefinition.spec.members[index] [Go to parent definition](#apidefinitionspec) @@ -4737,11 +4789,22 @@ from an API instance or from a config map created in the cluster (which is the d Default: []
false + + groupRefs + []object + + List of group references associated with the API +This groups are references to Group custom resources created on the cluster.
+
+ Default: []
+ + false groups []string - List of groups associated with the API
+ List of groups associated with the API. +This groups are id or name references to existing groups in APIM.

Default: []
@@ -6846,6 +6909,47 @@ Reference to an existing Shared Policy Group Reference to an existing Shared Policy Group + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring +
+
true
kindstring +
+
false
namespacestring +
+
false
+ + +### ApiV4Definition.spec.groupRefs[index] +[Go to parent definition](#apiv4definitionspec) + + + + + diff --git a/examples/apim/group/group.yml b/examples/apim/group/group.yml new file mode 100644 index 000000000..a5d8a2ec9 --- /dev/null +++ b/examples/apim/group/group.yml @@ -0,0 +1,31 @@ +# +# Copyright (C) 2015 The Gravitee team (http://gravitee.io) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +apiVersion: gravitee.io/v1alpha1 +kind: Group +metadata: + name: developers +spec: + contextRef: + name: "dev-ctx" + name: "developers" + notifyMembers: false + members: + - source: memory + sourceId: api1 + roles: + API: OWNER + APPLICATION: OWNER + INTEGRATION: USER diff --git a/helm/gko/crds/gravitee.io_apidefinitions.yaml b/helm/gko/crds/gravitee.io_apidefinitions.yaml index c3ba118bc..e5b4736c4 100644 --- a/helm/gko/crds/gravitee.io_apidefinitions.yaml +++ b/helm/gko/crds/gravitee.io_apidefinitions.yaml @@ -267,9 +267,28 @@ spec: description: The definition version of the API. For v1alpha1 resources, this field should always set to `2.0.0`. type: string + groupRefs: + default: [] + description: |- + List of group references associated with the API + This groups are references to Group custom resources created on the cluster. + items: + properties: + kind: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array groups: default: [] - description: List of groups associated with the API + description: |- + List of groups associated with the API. + This groups are id or name references to existing groups in APIM. items: type: string type: array diff --git a/helm/gko/crds/gravitee.io_apiv4definitions.yaml b/helm/gko/crds/gravitee.io_apiv4definitions.yaml index 17a3b6707..106ed1fe5 100644 --- a/helm/gko/crds/gravitee.io_apiv4definitions.yaml +++ b/helm/gko/crds/gravitee.io_apiv4definitions.yaml @@ -869,9 +869,28 @@ spec: - enabled type: object type: array + groupRefs: + default: [] + description: |- + List of group references associated with the API + This groups are references to Group custom resources created on the cluster. + items: + properties: + kind: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array groups: default: [] - description: List of groups associated with the API + description: |- + List of groups associated with the API. + This groups are id or name references to existing groups in APIM. items: type: string type: array diff --git a/helm/gko/crds/gravitee.io_groups.yaml b/helm/gko/crds/gravitee.io_groups.yaml new file mode 100644 index 000000000..68a504ab9 --- /dev/null +++ b/helm/gko/crds/gravitee.io_groups.yaml @@ -0,0 +1,151 @@ +# Copyright (C) 2015 The Gravitee team (http://gravitee.io) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: groups.gravitee.io +spec: + group: gravitee.io + names: + kind: Group + listKind: GroupList + plural: groups + singular: group + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The number of members added to the group + jsonPath: .status.members + name: Members at + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + 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 + 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 + metadata: + type: object + spec: + properties: + contextRef: + properties: + kind: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + id: + type: string + members: + items: + properties: + roles: + additionalProperties: + type: string + default: {} + type: object + source: + description: Member source + example: gravitee + type: string + sourceId: + description: Member source ID + example: user@email.com + type: string + required: + - source + - sourceId + type: object + type: array + name: + type: string + notifyMembers: + default: true + description: |- + If true, new members added to the API spec will + be notified when the API is synced with APIM. + type: boolean + required: + - members + - name + type: object + status: + properties: + environmentId: + description: The environment ID defined in the management context + type: string + errors: + description: |- + When group has been created regardless of errors, this field is + used to persist the error message encountered during admission + properties: + severe: + description: |- + severe errors do not pass admission and will block reconcile + hence, this field should always be during the admission phase + and is very unlikely to be persisted in the status + items: + type: string + type: array + warning: + description: |- + warning errors do not block object reconciliation, + most of the time because the value is ignored or defaulted + when the API gets synced with APIM + items: + type: string + type: array + type: object + id: + description: The ID of the Group in the Gravitee API Management instance + type: string + members: + description: The number of members added to this group + type: integer + organizationId: + description: The organization ID defined in the management context + type: string + processingStatus: + description: The processing status of the Group. + type: string + required: + - members + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/gko/templates/rbac/manager-cluster-role.yaml b/helm/gko/templates/rbac/manager-cluster-role.yaml index 51f67526e..02a97df74 100644 --- a/helm/gko/templates/rbac/manager-cluster-role.yaml +++ b/helm/gko/templates/rbac/manager-cluster-role.yaml @@ -211,6 +211,33 @@ rules: - gravitee.io resources: - subscriptions/status + verbs: + - get + - patch + - update + - apiGroups: + - gravitee.io + resources: + - groups + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - gravitee.io + resources: + - groups/finalizers + verbs: + - update + - apiGroups: + - gravitee.io + resources: + - groups/status verbs: - get - patch diff --git a/helm/gko/templates/rbac/manager-role.yaml b/helm/gko/templates/rbac/manager-role.yaml index 2f87cecfa..ff3594934 100644 --- a/helm/gko/templates/rbac/manager-role.yaml +++ b/helm/gko/templates/rbac/manager-role.yaml @@ -210,6 +210,33 @@ rules: - gravitee.io resources: - subscriptions/status + verbs: + - get + - patch + - update + - apiGroups: + - gravitee.io + resources: + - groups + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - gravitee.io + resources: + - groups/finalizers + verbs: + - update + - apiGroups: + - gravitee.io + resources: + - groups/status verbs: - get - patch diff --git a/helm/gko/templates/rbac/resource-patch-cluster-role.yaml b/helm/gko/templates/rbac/resource-patch-cluster-role.yaml index 4978b4ec5..02a2a8b1b 100644 --- a/helm/gko/templates/rbac/resource-patch-cluster-role.yaml +++ b/helm/gko/templates/rbac/resource-patch-cluster-role.yaml @@ -45,6 +45,7 @@ rules: - apiresources.gravitee.io - subscriptions.gravitee.io - sharedpolicygroups.gravitee.io + - groups.gravitee.io resources: - customresourcedefinitions verbs: diff --git a/helm/gko/templates/webhook/mutation-webhook.yaml b/helm/gko/templates/webhook/mutation-webhook.yaml index c24ca16af..2ce7e000a 100644 --- a/helm/gko/templates/webhook/mutation-webhook.yaml +++ b/helm/gko/templates/webhook/mutation-webhook.yaml @@ -81,4 +81,31 @@ webhooks: timeoutSeconds: 10 admissionReviewVersions: - v1 +- name: v1alpha1.gravitee.io.group + clientConfig: + service: + namespace: {{ .Release.Namespace }} + name: {{ .Values.manager.webhook.service.name }} + path: /mutate-gravitee-io-v1alpha1-group + port: 443 + failurePolicy: Fail + matchPolicy: Equivalent + objectSelector: {} + reinvocationPolicy: Never + rules: + - apiGroups: + - gravitee.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - groups + scope: '*' + namespaceSelector: {} + sideEffects: None + timeoutSeconds: 10 + admissionReviewVersions: + - v1 {{- end }} diff --git a/helm/gko/templates/webhook/validation-webhook.yaml b/helm/gko/templates/webhook/validation-webhook.yaml index d13e9bb94..cba26cc7c 100644 --- a/helm/gko/templates/webhook/validation-webhook.yaml +++ b/helm/gko/templates/webhook/validation-webhook.yaml @@ -189,6 +189,33 @@ webhooks: timeoutSeconds: 10 admissionReviewVersions: - v1 + - name: v1alpha1.gravitee.io.group + clientConfig: + service: + namespace: {{ .Release.Namespace }} + name: {{ .Values.manager.webhook.service.name }} + path: /validate-gravitee-io-v1alpha1-group + port: 443 + rules: + - operations: + - CREATE + - UPDATE + - DELETE + apiGroups: + - gravitee.io + apiVersions: + - v1alpha1 + resources: + - 'groups' + scope: '*' + failurePolicy: Fail + matchPolicy: Equivalent + namespaceSelector: {} + objectSelector: {} + sideEffects: None + timeoutSeconds: 10 + admissionReviewVersions: + - v1 - name: v1alpha1.gravitee.io.sharedpolicygroups clientConfig: service: diff --git a/internal/admission/group/crtl.go b/internal/admission/group/crtl.go new file mode 100644 index 000000000..944d33c4c --- /dev/null +++ b/internal/admission/group/crtl.go @@ -0,0 +1,60 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + ctrl "sigs.k8s.io/controller-runtime" +) + +var _ admission.CustomValidator = AdmissionCtrl{} +var _ admission.CustomDefaulter = AdmissionCtrl{} + +func (a AdmissionCtrl) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha1.Group{}). + WithValidator(a). + WithDefaulter(a). + Complete() +} + +type AdmissionCtrl struct{} + +func (a AdmissionCtrl) Default(ctx context.Context, obj runtime.Object) error { + return nil +} + +func (a AdmissionCtrl) ValidateCreate( + ctx context.Context, obj runtime.Object, +) (admission.Warnings, error) { + return validateCreate(ctx, obj).Map() +} + +func (a AdmissionCtrl) ValidateDelete( + ctx context.Context, obj runtime.Object, +) (admission.Warnings, error) { + return admission.Warnings{}, nil +} + +func (a AdmissionCtrl) ValidateUpdate( + ctx context.Context, oldObj runtime.Object, newObj runtime.Object, +) (admission.Warnings, error) { + return validateCreate(ctx, newObj).Map() +} diff --git a/internal/admission/group/validate.go b/internal/admission/group/validate.go new file mode 100644 index 000000000..3dff63a63 --- /dev/null +++ b/internal/admission/group/validate.go @@ -0,0 +1,67 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/ctxref" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "k8s.io/apimachinery/pkg/runtime" +) + +func validateCreate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + if group, ok := obj.(*v1alpha1.Group); ok { + errs.Add(ctxref.Validate(ctx, group)) + if errs.IsSevere() { + return errs + } + errs.MergeWith(validateDryRun(ctx, group)) + } + return errs +} + +func validateDryRun(ctx context.Context, group *v1alpha1.Group) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + + cp := group.DeepCopy() + + apim, err := apim.FromContextRef(ctx, cp.ContextRef(), cp.GetNamespace()) + if err != nil { + errs.AddSevere(err.Error()) + return errs + } + + cp.PopulateIDs(apim.Context) + + status, err := apim.Env.DryRunImportGroup(cp.Spec.Type) + if err != nil { + errs.AddSevere(err.Error()) + return errs + } + + for _, severe := range status.Errors.Severe { + errs.AddSevere(severe) + } + + for _, warning := range status.Errors.Warning { + errs.AddWarning(warning) + } + + return errs +} diff --git a/internal/apim/model/env.go b/internal/apim/model/env.go index c317809c5..16db7074b 100644 --- a/internal/apim/model/env.go +++ b/internal/apim/model/env.go @@ -24,6 +24,10 @@ type Group struct { Name string `json:"name,omitempty"` } +type GroupStatus struct { + Members uint `json:"members"` +} + type Category struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` diff --git a/internal/apim/service/env.go b/internal/apim/service/env.go index 3afde8f8c..9ade2d1e2 100644 --- a/internal/apim/service/env.go +++ b/internal/apim/service/env.go @@ -15,6 +15,9 @@ package service import ( + "strconv" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/client" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" ) @@ -39,6 +42,30 @@ func (svc *Env) CreateCategory(category *model.Category) error { return svc.HTTP.Post(url.String(), category, category) } +func (svc *Env) DryRunImportGroup(spec *group.Type) (*group.Status, error) { + return svc.importGroup(spec, true) +} + +func (svc *Env) ImportGroup(spec *group.Type) (*group.Status, error) { + return svc.importGroup(spec, false) +} + +func (svc *Env) importGroup(spec *group.Type, dryRun bool) (*group.Status, error) { + url := svc.EnvV2Target("groups"). + WithPath("_import").WithPath("crd"). + WithQueryParam("dryRun", strconv.FormatBool(dryRun)) + status := new(group.Status) + if err := svc.HTTP.Put(url.String(), spec, status); err != nil { + return nil, err + } + return status, nil +} + +func (svc *Env) DeleteGroup(id string) error { + url := svc.EnvV1Target("configuration").WithPath("groups").WithPath(id) + return svc.HTTP.Delete(url.String(), nil) +} + func (svc *Env) Get() (*model.Env, error) { env := new(model.Env) if err := svc.HTTP.Get(svc.URLs.EnvV2.String(), env); err != nil { diff --git a/internal/core/interface.go b/internal/core/interface.go index 66ceaf492..b7610a161 100644 --- a/internal/core/interface.go +++ b/internal/core/interface.go @@ -42,6 +42,7 @@ type Object interface { GetSpec() Spec GetStatus() Status GetRef() ObjectRef + IsBeingDeleted() bool } // +k8s:deepcopy-gen=false @@ -77,6 +78,9 @@ type ApiDefinitionModel interface { GetPlan(string) PlanModel IsStopped() bool GetType() string + GetGroups() []string + SetGroups([]string) + GetGroupRefs() []ObjectRef } // +k8s:deepcopy-gen=false diff --git a/internal/core/keys.go b/internal/core/keys.go index 8da44ff13..0847ee6f1 100644 --- a/internal/core/keys.go +++ b/internal/core/keys.go @@ -45,6 +45,7 @@ const ( KeyPairFinalizer = "finalizers.gravitee.io/keypair" ApplicationFinalizer = "finalizers.gravitee.io/applicationdeletion" SubscriptionFinalizer = "finalizers.gravitee.io/subscriptions" + GroupFinalizer = "finalizers.gravitee.io/groups" TemplatingFinalizer = "finalizers.gravitee.io/templating" SharedPolicyGroupFinalizer = "finalizers.gravitee.io/sharedpolicygroups" diff --git a/internal/log/log.go b/internal/log/log.go index 9a78e1525..92640b794 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -17,6 +17,8 @@ package log import ( "context" "fmt" + "io" + "os" "strings" stdLog "log" @@ -30,6 +32,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + kZap "sigs.k8s.io/controller-runtime/pkg/log/zap" ) var sink *zap.Logger @@ -156,12 +159,21 @@ func init() { OutputPaths: []string{"stdout"}, } sink = zap.Must(config.Build()) - logger := zapr.NewLogger(sink) - ctrl.SetLogger(logger) - log.SetLogger(logger) - kLog.SetLogger(logger) - Global = raw{sink: sink} - stdLog.SetOutput(&Global) + if isSilent() { + logger := kZap.New(kZap.WriteTo(io.Discard), kZap.UseDevMode(true)) + ctrl.SetLogger(logger) + log.SetLogger(logger) + kLog.SetLogger(logger) + stdLog.SetOutput(io.Discard) + Global = raw{sink: sink} + } else { + logger := zapr.NewLogger(sink) + ctrl.SetLogger(logger) + log.SetLogger(logger) + kLog.SetLogger(logger) + Global = raw{sink: sink} + stdLog.SetOutput(&Global) + } } func getEncoding() string { @@ -214,3 +226,8 @@ func getTimeEncoder() zapcore.TimeEncoder { return zapcore.ISO8601TimeEncoder } } + +func isSilent() bool { + silent := os.Getenv("GKO_MANAGER_SILENT_LOG") + return silent == env.TrueString +} diff --git a/internal/predicate/predicate.go b/internal/predicate/predicate.go index 2801bc877..7b953486c 100644 --- a/internal/predicate/predicate.go +++ b/internal/predicate/predicate.go @@ -53,6 +53,8 @@ func (LastSpecHashPredicate) Create(e event.CreateEvent) bool { return e.Object.GetAnnotations()[core.LastSpecHashAnnotation] != hash.Calculate(&t.Spec) case *v1alpha1.SharedPolicyGroup: return e.Object.GetAnnotations()[core.LastSpecHashAnnotation] != hash.Calculate(&t.Spec) + case *v1alpha1.Group: + return e.Object.GetAnnotations()[core.LastSpecHashAnnotation] != hash.Calculate(&t.Spec) case *netV1.Ingress: return e.Object.GetAnnotations()[core.LastSpecHashAnnotation] != hash.Calculate(&t.Spec) case *corev1.Secret: @@ -94,6 +96,9 @@ func (LastSpecHashPredicate) Update(e event.UpdateEvent) bool { case *v1alpha1.SharedPolicyGroup: oo, _ := e.ObjectOld.(*v1alpha1.SharedPolicyGroup) return hash.Calculate(&no.Spec) != hash.Calculate(&oo.Spec) + case *v1alpha1.Group: + oo, _ := e.ObjectOld.(*v1alpha1.Group) + return hash.Calculate(&no.Spec) != hash.Calculate(&oo.Spec) case *netV1.Ingress: oo, _ := e.ObjectOld.(*netV1.Ingress) return hash.Calculate(&no.Spec) != hash.Calculate(&oo.Spec) diff --git a/main.go b/main.go index 2e1afabcd..91f6410c6 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( v2Admission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v2" v4Admission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" appAdmission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/application" + groupAdmission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/group" mctxAdmission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" spgAdmission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/policygroups" resourceAdmission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/resource" @@ -48,6 +49,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/application" + "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/group" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/secrets" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/subscription" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/env" @@ -261,6 +263,14 @@ func registerControllers(mgr manager.Manager) { os.Exit(1) } + if err := (&group.Reconciler{ + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("group-controller"), + }).SetupWithManager(mgr); err != nil { + log.Global.Error(err, "Unable to create controller for groups") + os.Exit(1) + } + if err := (&policygroups.Reconciler{ Scheme: mgr.GetScheme(), Client: mgr.GetClient(), @@ -313,7 +323,7 @@ func applyCRDs() error { crd := &unstructured.Unstructured{Object: obj} if crd, err = client.Resource(version).Apply(ctx, crd.GetName(), crd, opts); err == nil { - log.Global.Infof("Applied resource definition [%s]", crd) + log.Global.Infof("Applied resource definition [%s]", crd.GetName()) } return err @@ -382,5 +392,8 @@ func setupAdmissionWebhooks(mgr manager.Manager) error { if err := (spgAdmission.AdmissionCtrl{}).SetupWithManager(mgr); err != nil { return err } + if err := (groupAdmission.AdmissionCtrl{}).SetupWithManager(mgr); err != nil { + return err + } return nil } diff --git a/test/integration/admission/group/suite_test.go b/test/integration/admission/group/suite_test.go new file mode 100644 index 000000000..4b59aa615 --- /dev/null +++ b/test/integration/admission/group/suite_test.go @@ -0,0 +1,43 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "testing" + "time" + + "github.com/onsi/gomega/gexec" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + //+kubebuilder:scaffold:imports +) + +func TestResources(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Group Admission Suite") +} + +var _ = SynchronizedBeforeSuite(func() { + // NOSONAR mandatory noop +}, func() { +}) + +var _ = SynchronizedAfterSuite(func() { + By("Tearing down the test environment") + gexec.KillAndWait(5 * time.Second) +}, func() { + // NOSONAR ignore this noop func +}) diff --git a/test/integration/admission/group/update_withContext_andPrimaryOwner_test.go b/test/integration/admission/group/update_withContext_andPrimaryOwner_test.go new file mode 100644 index 000000000..ad0831759 --- /dev/null +++ b/test/integration/admission/group/update_withContext_andPrimaryOwner_test.go @@ -0,0 +1,86 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package group + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" + adm "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/group" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/manager" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/random" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Validate update", labels.WithContext, func() { + interval := constants.Interval + timeout := constants.EventualTimeout + ctx := context.Background() + + admissionCtrl := adm.AdmissionCtrl{} + + DescribeTable("should return severe error with primary owner role", + func(roleScope group.RoleScope) { + fixtures := fixture. + Builder(). + WithContext(constants.ContextWithCredentialsFile). + WithGroup(constants.GroupFile). + Build() + + By("initializing a service account in current organization") + + apim := apim.NewClient(ctx) + saName := random.GetName() + + Expect(apim.Org.CreateUser(model.NewServiceAccount(saName))).To(Succeed()) + + By("adding the sa to the Group") + + groupMember := base.NewGraviteeGroupMember(saName, "OWNER") + fixtures.Group.Spec.Members = []group.Member{groupMember} + + fixtures = fixtures.Apply() + obj := fixtures.Group + newObj := obj.DeepCopy() + + By("adding the sa role to be primary owner for scope " + string(roleScope)) + + newObj.Spec.Members[0].Roles[roleScope] = "PRIMARY_OWNER" + + Eventually(func() error { + _, err := admissionCtrl.ValidateUpdate(ctx, obj, newObj) + return assert.Equals( + "error", + errors.NewSevere("setting a member with the primary owner role is not allowed"), + err, + ) + }, timeout, interval).Should(Succeed(), fixtures.Group.Name) + + Expect(manager.Delete(ctx, fixtures.Group)).To(Succeed()) + }, + Entry("on API role scope", group.APIRoleScope), + Entry("on application role scope", group.ApplicationRoleScope), + Entry("on integration role scope", group.APIRoleScope), + ) +}) diff --git a/test/integration/apidefinition/v2/create_withContext_andGroupRef_test.go b/test/integration/apidefinition/v2/create_withContext_andGroupRef_test.go new file mode 100644 index 000000000..5dd2d4e24 --- /dev/null +++ b/test/integration/apidefinition/v2/create_withContext_andGroupRef_test.go @@ -0,0 +1,87 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v2 + +import ( + "context" + "strings" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/random" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create", labels.WithContext, func() { + timeout := constants.EventualTimeout + interval := constants.Interval + + ctx := context.Background() + + It("should add group reference to API", func() { + fixtures := fixture. + Builder(). + WithContext(constants.ContextWithCredentialsFile). + WithGroup(constants.GroupFile). + WithAPI(constants.ApiWithContextFile). + Build() + + By("initializing a service account in current organization") + + apim := apim.NewClient(ctx) + saName := random.GetName() + Expect(apim.Org.CreateUser(model.NewServiceAccount(saName))).To(Succeed()) + + By("adding the sa to the Group") + + groupMember := base.NewGraviteeGroupMember(saName, "OWNER") + fixtures.Group.Spec.Members = []group.Member{groupMember} + + fixtures.API.Spec.GroupRefs = []refs.NamespacedName{refs.NewNamespacedName( + fixtures.Group.Namespace, + fixtures.Group.Name, + )} + + fixtures = fixtures.Apply() + + By("checking that group has been assigned in exported API") + + expectedGroups := []string{fixtures.Group.Spec.Name} + + Eventually(func() error { + apiExport, err := apim.Export.V2Api(fixtures.API.Status.ID) + if err != nil { + return err + } + + apiGroups := apiExport.Spec.Groups + + return assert.SliceEqualsSorted( + "groups", + expectedGroups, apiGroups, + strings.Compare, + ) + }, timeout, interval).Should(Succeed(), fixtures.API.Name) + }) +}) diff --git a/test/integration/apidefinition/v4/create_withContext_andGroupRef_test.go b/test/integration/apidefinition/v4/create_withContext_andGroupRef_test.go new file mode 100644 index 000000000..bf46fc702 --- /dev/null +++ b/test/integration/apidefinition/v4/create_withContext_andGroupRef_test.go @@ -0,0 +1,87 @@ +// Copyright (C) 2015 The Gravitee team (http://gravitee.io) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v4 + +import ( + "context" + "strings" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/group" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/random" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create", labels.WithContext, func() { + timeout := constants.EventualTimeout + interval := constants.Interval + + ctx := context.Background() + + It("should add group reference to API", func() { + fixtures := fixture. + Builder(). + WithContext(constants.ContextWithCredentialsFile). + WithGroup(constants.GroupFile). + WithAPIv4(constants.ApiV4WithContextFile). + Build() + + By("initializing a service account in current organization") + + apim := apim.NewClient(ctx) + saName := random.GetName() + Expect(apim.Org.CreateUser(model.NewServiceAccount(saName))).To(Succeed()) + + By("adding the sa to the Group") + + groupMember := base.NewGraviteeGroupMember(saName, "OWNER") + fixtures.Group.Spec.Members = []group.Member{groupMember} + + fixtures.APIv4.Spec.GroupRefs = []refs.NamespacedName{refs.NewNamespacedName( + fixtures.Group.Namespace, + fixtures.Group.Name, + )} + + fixtures = fixtures.Apply() + + By("checking that group has been assigned in exported API") + + expectedGroups := []string{fixtures.Group.Spec.Name} + + Eventually(func() error { + apiExport, err := apim.Export.V4Api(fixtures.APIv4.Status.ID) + if err != nil { + return err + } + + apiGroups := apiExport.Spec.Groups + + return assert.SliceEqualsSorted( + "groups", + expectedGroups, apiGroups, + strings.Compare, + ) + }, timeout, interval).Should(Succeed(), fixtures.APIv4.Name) + }) +}) diff --git a/test/internal/integration/assert/assert.go b/test/internal/integration/assert/assert.go index 9303aa839..e32e47110 100644 --- a/test/internal/integration/assert/assert.go +++ b/test/internal/integration/assert/assert.go @@ -83,6 +83,14 @@ func SharedPolicyGroupFailed(sub *v1alpha1.SharedPolicyGroup) error { return Equals(reconcileStatus, core.ProcessingStatusFailed, sub.Status.ProcessingStatus) } +func GroupCompleted(group *v1alpha1.Group) error { + return Equals(reconcileStatus, core.ProcessingStatusCompleted, group.Status.ProcessingStatus) +} + +func GroupFailed(group *v1alpha1.Group) error { + return Equals(reconcileStatus, core.ProcessingStatusFailed, group.Status.ProcessingStatus) +} + func ApiFailed(apiDefinition *v1alpha1.ApiDefinition) error { return Equals(reconcileStatus, core.ProcessingStatusFailed, apiDefinition.Status.ProcessingStatus) } diff --git a/test/internal/integration/constants/constants.go b/test/internal/integration/constants/constants.go index e1200eff5..02d7ba7f0 100644 --- a/test/internal/integration/constants/constants.go +++ b/test/internal/integration/constants/constants.go @@ -151,6 +151,8 @@ const ( SharedPolicyGroupsFile = "apim/shared_policy_groups/shared_policy_groups.yml" + GroupFile = "apim/group/group.yml" + // Use cases. SubscribeJWTUseCaseContextFile = "usecase/subscribe-to-jwt-plan/resources/management-context.yml" SubscribeJWTUseCaseAPIFile = "usecase/subscribe-to-jwt-plan/resources/api.yml" diff --git a/test/internal/integration/fixture/apply.go b/test/internal/integration/fixture/apply.go index dd276727f..b17ec7a7b 100644 --- a/test/internal/integration/fixture/apply.go +++ b/test/internal/integration/fixture/apply.go @@ -43,6 +43,10 @@ func (o *Objects) Apply() *Objects { o.applyContext(cli, ctx) } + if o.Group != nil { + o.applyGroup(ctx, cli) + } + if o.Resource != nil { o.applyResource(cli, ctx) } @@ -169,6 +173,20 @@ func (o *Objects) applySubscription(cli client.Client, ctx context.Context) { }, constants.EventualTimeout, constants.Interval).Should(Succeed(), o.Subscription.Name) } +func (o *Objects) applyGroup(ctx context.Context, cli client.Client) { + Expect(cli.Create(ctx, o.Group)).ToNot(HaveOccurred()) + Eventually(ctx, func() error { + err := manager.GetLatest(ctx, o.Group) + if err != nil { + return err + } + if err = assert.GroupCompleted(o.Group); err != nil { + return assert.GroupFailed(o.Group) + } + return nil + }, constants.EventualTimeout, constants.Interval).Should(Succeed(), o.Group.Name) +} + func (o *Objects) applySharedPolicyGroup(cli client.Client, ctx context.Context) { Expect(cli.Create(ctx, o.SharedPolicyGroup)).ToNot(HaveOccurred()) Eventually(ctx, func() error { diff --git a/test/internal/integration/fixture/build.go b/test/internal/integration/fixture/build.go index e21ec590b..784590392 100644 --- a/test/internal/integration/fixture/build.go +++ b/test/internal/integration/fixture/build.go @@ -40,6 +40,7 @@ type Files struct { Ingress string Subscription string SharedPolicyGroups string + Group string } type FSBuilder struct { @@ -85,6 +86,10 @@ func (b *FSBuilder) Build() *Objects { setupSharedPolicyGroup(obj, sub, suffix) } + if group := decodeIfDefined(f.Group, &v1alpha1.Group{}, groupKind); group != nil { + setUpGroup(obj, group, suffix) + } + if ctx := decodeIfDefined(f.Context, &v1alpha1.ManagementContext{}, ctxKind); ctx != nil { setupMgmtContext(obj, ctx, suffix) } else { @@ -155,6 +160,9 @@ func setupMgmtContext(obj *Objects, ctx **v1alpha1.ManagementContext, suffix str if obj.Application != nil { obj.Application.Spec.Context = obj.Context.GetNamespacedName() } + if obj.Group != nil { + obj.Group.Spec.Context = obj.Context.GetNamespacedName() + } if obj.SharedPolicyGroup != nil { obj.SharedPolicyGroup.Spec.Context = obj.Context.GetNamespacedName() } @@ -204,6 +212,13 @@ func setupSubscription(obj *Objects, sub **v1alpha1.Subscription, suffix string) obj.Subscription.Spec.App.Name += suffix } +func setUpGroup(obj *Objects, group **v1alpha1.Group, suffix string) { + obj.Group = *group + obj.Group.Name += suffix + obj.Group.Spec.Name += suffix + obj.Group.Namespace = constants.Namespace +} + func setupSharedPolicyGroup(obj *Objects, sub **v1alpha1.SharedPolicyGroup, suffix string) { obj.SharedPolicyGroup = *sub obj.SharedPolicyGroup.Name += suffix @@ -301,6 +316,11 @@ func (b *FSBuilder) WithSharedPolicyGroups(file string) *FSBuilder { return b } +func (b *FSBuilder) WithGroup(file string) *FSBuilder { + b.files.Group = file + return b +} + func (b *FSBuilder) WithIngress(file string) *FSBuilder { b.files.Ingress = file return b diff --git a/test/internal/integration/fixture/decode.go b/test/internal/integration/fixture/decode.go index fe150c22f..6990f439b 100644 --- a/test/internal/integration/fixture/decode.go +++ b/test/internal/integration/fixture/decode.go @@ -39,6 +39,7 @@ var ( apiKind = v1alpha1.GroupVersion.WithKind("ApiDefinition") apiV4Kind = v1alpha1.GroupVersion.WithKind("ApiV4Definition") subscriptionKind = v1alpha1.GroupVersion.WithKind("Subscription") + groupKind = v1alpha1.GroupVersion.WithKind("Group") sharedPolicyGroupsKind = v1alpha1.GroupVersion.WithKind("SharedPolicyGroup") ) diff --git a/test/internal/integration/fixture/objects.go b/test/internal/integration/fixture/objects.go index 7fb458852..dd44d90d7 100644 --- a/test/internal/integration/fixture/objects.go +++ b/test/internal/integration/fixture/objects.go @@ -34,6 +34,7 @@ type Objects struct { APIv4 *v1alpha1.ApiV4Definition Application *v1alpha1.Application Subscription *v1alpha1.Subscription + Group *v1alpha1.Group SharedPolicyGroup *v1alpha1.SharedPolicyGroup randomSuffix string diff --git a/test/internal/integration/manager/manager.go b/test/internal/integration/manager/manager.go index da474e45b..556bcb848 100644 --- a/test/internal/integration/manager/manager.go +++ b/test/internal/integration/manager/manager.go @@ -16,22 +16,19 @@ package manager import ( "context" - "io" "os" policygroups "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/policygroups" v1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/apidefinition" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/apiresource" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/application" + "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/group" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/ingress" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/managementcontext" "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/secrets" @@ -41,8 +38,6 @@ import ( "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/watch" - . "github.com/onsi/ginkgo/v2" - ctrl "sigs.k8s.io/controller-runtime" metrics "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -70,12 +65,6 @@ func Instance() ctrl.Manager { func init() { os.Setenv(env.HttpCLientInsecureSkipCertVerify, env.TrueString) - if _, r := GinkgoConfiguration(); r.Verbose { - log.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - } else { - log.SetLogger(zap.New(zap.WriteTo(io.Discard), zap.UseDevMode(true))) - } - ctx := context.Background() scheme := clientScheme.Scheme @@ -176,6 +165,11 @@ func init() { Recorder: mgr.GetEventRecorderFor("subscription-controller"), }).SetupWithManager(mgr)) + runtimeUtil.Must((&group.Reconciler{ + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("group-controller"), + }).SetupWithManager(mgr)) + runtimeUtil.Must((&policygroups.Reconciler{ Scheme: mgr.GetScheme(), Client: mgr.GetClient(),