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)
+
+
+
+
+
+
+
+
+ Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ name |
+ string |
+
+
+ |
+ true |
+
+ kind |
+ string |
+
+
+ |
+ false |
+
+ namespace |
+ string |
+
+
+ |
+ 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
+
+
+
+ Name |
+ Type |
+ Description |
+ Required |
+
+
+
+ name |
+ string |
+
+
+ |
+ true |
+
+ kind |
+ string |
+
+
+ |
+ false |
+
+ namespace |
+ string |
+
+
+ |
+ 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(),