From ebe3740691d0501d23a9c49c341354b16b39cf70 Mon Sep 17 00:00:00 2001 From: Kamiel Ahmadpour Date: Thu, 9 Jan 2025 14:26:50 +0000 Subject: [PATCH] feat: add support for Shared Policy Groups see: https://gravitee.atlassian.net/browse/GKO-912 --- api/model/policygroups/sharedpolicygroups.go | 68 ++++ api/model/policygroups/status.go | 35 ++ .../policygroups/zz_generated.deepcopy.go | 134 ++++++++ api/v1alpha1/sharedpolicygroups_types.go | 151 ++++++++ api/v1alpha1/zz_generated.deepcopy.go | 101 ++++++ .../apim/policygroups/internal/delete.go | 55 +++ .../apim/policygroups/internal/status.go | 37 ++ .../apim/policygroups/internal/update.go | 42 +++ .../sharedpolicygroups_controller.go | 124 +++++++ crdoc/toc.yaml | 1 + docs/api/reference.md | 322 ++++++++++++++++++ .../shared_policy_groups.yml | 26 ++ .../crds/gravitee.io_sharedpolicygroups.yaml | 190 +++++++++++ internal/admission/policygroups/ctrl.go | 68 ++++ internal/admission/policygroups/validate.go | 107 ++++++ internal/apim/apim.go | 20 +- internal/apim/model/sharedpolicygroups.go | 34 ++ internal/apim/service/sharedpolicygroups.go | 101 ++++++ internal/core/interface.go | 5 + internal/core/keys.go | 1 + internal/log/log.go | 2 +- internal/predicate/predicate.go | 5 + internal/uuid/uuid.go | 27 ++ main.go | 11 + 24 files changed, 1657 insertions(+), 10 deletions(-) create mode 100644 api/model/policygroups/sharedpolicygroups.go create mode 100644 api/model/policygroups/status.go create mode 100644 api/model/policygroups/zz_generated.deepcopy.go create mode 100644 api/v1alpha1/sharedpolicygroups_types.go create mode 100644 controllers/apim/policygroups/internal/delete.go create mode 100644 controllers/apim/policygroups/internal/status.go create mode 100644 controllers/apim/policygroups/internal/update.go create mode 100644 controllers/apim/policygroups/sharedpolicygroups_controller.go create mode 100644 examples/apim/shared_policy_groups/shared_policy_groups.yml create mode 100644 helm/gko/crds/gravitee.io_sharedpolicygroups.yaml create mode 100644 internal/admission/policygroups/ctrl.go create mode 100644 internal/admission/policygroups/validate.go create mode 100644 internal/apim/model/sharedpolicygroups.go create mode 100644 internal/apim/service/sharedpolicygroups.go diff --git a/api/model/policygroups/sharedpolicygroups.go b/api/model/policygroups/sharedpolicygroups.go new file mode 100644 index 000000000..3ba9d4332 --- /dev/null +++ b/api/model/policygroups/sharedpolicygroups.go @@ -0,0 +1,68 @@ +// 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 policygroups + +import ( + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/utils" +) + +// +kubebuilder:validation:Enum=MESSAGE;PROXY;NATIVE; +type ApiType string + +// +kubebuilder:validation:Enum=REQUEST;RESPONSE;INTERACT;CONNECT;PUBLISH;SUBSCRIBE; +type FlowPhase string + +type SharedPolicyGroup struct { + // CrossID to export SharedPolicyGroup into different environments + CrossID *string `json:"crossId,omitempty"` + // SharedPolicyGroup name + // +kubebuilder:validation:Required + Name *string `json:"name"` + // SharedPolicyGroup description + Description *string `json:"description,omitempty"` + // SharedPolicyGroup prerequisite Message + PrerequisiteMessage *string `json:"prerequisiteMessage,omitempty"` + // Specify the SharedPolicyGroup ApiType + // +kubebuilder:validation:Required + ApiType *ApiType `json:"apiType"` + // SharedPolicyGroup phase (REQUEST;RESPONSE;INTERACT;CONNECT;PUBLISH;SUBSCRIBE) + // +kubebuilder:validation:Required + Phase *FlowPhase `json:"phase"` + // SharedPolicyGroup Steps + Steps []*Step `json:"steps,omitempty"` + // SharedPolicyGroup LifecycleState (UNDEPLOYED;DEPLOYED;PENDING) +} + +type Step struct { + // +kubebuilder:default:=true + // Indicate if this FlowStep is enabled or not + Enabled bool `json:"enabled"` + // FlowStep policy + // +kubebuilder:validation:Optional + Policy *string `json:"policy,omitempty"` + // FlowStep name + // +kubebuilder:validation:Optional + Name *string `json:"name,omitempty"` + // FlowStep description + // +kubebuilder:validation:Optional + Description *string `json:"description,omitempty"` + // FlowStep configuration is a map of arbitrary key-values + // +kubebuilder:validation:Optional + Configuration *utils.GenericStringMap `json:"configuration,omitempty"` + // FlowStep condition + // +kubebuilder:validation:Optional + Condition *string `json:"condition,omitempty"` +} diff --git a/api/model/policygroups/status.go b/api/model/policygroups/status.go new file mode 100644 index 000000000..2e16a1b5b --- /dev/null +++ b/api/model/policygroups/status.go @@ -0,0 +1,35 @@ +// 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 policygroups + +import ( + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/status" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" +) + +type Status struct { + // The organization ID, if a management context has been defined to sync with an APIM instance + OrgID string `json:"organizationId,omitempty"` + // The environment ID, if a management context has been defined to sync with an APIM instance + EnvID string `json:"environmentId,omitempty"` + // The Cross ID is used to identify an SharedPolicyGroup that has been promoted from one environment to another. + CrossID string `json:"crossId,omitempty"` + // The processing status of the SharedPolicyGroup. + // The value is `Completed` if the sync with APIM succeeded, Failed otherwise. + ProcessingStatus core.ProcessingStatus `json:"processingStatus,omitempty"` + // When SharedPolicyGroup 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/policygroups/zz_generated.deepcopy.go b/api/model/policygroups/zz_generated.deepcopy.go new file mode 100644 index 000000000..413ce3c6c --- /dev/null +++ b/api/model/policygroups/zz_generated.deepcopy.go @@ -0,0 +1,134 @@ +//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 policygroups + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SharedPolicyGroup) DeepCopyInto(out *SharedPolicyGroup) { + *out = *in + if in.CrossID != nil { + in, out := &in.CrossID, &out.CrossID + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.PrerequisiteMessage != nil { + in, out := &in.PrerequisiteMessage, &out.PrerequisiteMessage + *out = new(string) + **out = **in + } + if in.ApiType != nil { + in, out := &in.ApiType, &out.ApiType + *out = new(ApiType) + **out = **in + } + if in.Phase != nil { + in, out := &in.Phase, &out.Phase + *out = new(FlowPhase) + **out = **in + } + if in.Steps != nil { + in, out := &in.Steps, &out.Steps + *out = make([]*Step, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Step) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharedPolicyGroup. +func (in *SharedPolicyGroup) DeepCopy() *SharedPolicyGroup { + if in == nil { + return nil + } + out := new(SharedPolicyGroup) + 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 *Step) DeepCopyInto(out *Step) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = (*in).DeepCopy() + } + if in.Condition != nil { + in, out := &in.Condition, &out.Condition + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Step. +func (in *Step) DeepCopy() *Step { + if in == nil { + return nil + } + out := new(Step) + in.DeepCopyInto(out) + return out +} diff --git a/api/v1alpha1/sharedpolicygroups_types.go b/api/v1alpha1/sharedpolicygroups_types.go new file mode 100644 index 000000000..d69b1b819 --- /dev/null +++ b/api/v1alpha1/sharedpolicygroups_types.go @@ -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. + */ + +package v1alpha1 + +import ( + "fmt" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/policygroups" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/core" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/hash" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ core.SharedPolicyGroupObject = &SharedPolicyGroup{} + +// SharedPolicyGroupSpec +// +kubebuilder:object:generate=true +type SharedPolicyGroupSpec struct { + *policygroups.SharedPolicyGroup `json:",inline"` + // +kubebuilder:validation:Required + Context *refs.NamespacedName `json:"contextRef"` +} + +// Hash implements custom.Spec. +func (spec *SharedPolicyGroupSpec) Hash() string { + return hash.Calculate(spec) +} + +// SharedPolicyGroupSpecStatus defines the observed state of an API Context. +type SharedPolicyGroupSpecStatus struct { + policygroups.Status `json:",inline"` +} + +// SharedPolicyGroup +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="name",type=string,JSONPath=`.spec.name` +// +kubebuilder:printcolumn:name="description",type=string,JSONPath=`.spec.description` +// +kubebuilder:printcolumn:name="apiType",type=string,JSONPath=`.spec.apiType` +// +kubebuilder:resource:shortName=sharedpolicygroups +// +kubebuilder:storageversion +type SharedPolicyGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SharedPolicyGroupSpec `json:"spec,omitempty"` + Status SharedPolicyGroupSpecStatus `json:"status,omitempty"` +} + +// SharedPolicyGroupList contains a list of shared policy groups. +// +kubebuilder:object:root=true +type SharedPolicyGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SharedPolicyGroup `json:"items"` +} + +func (s *SharedPolicyGroup) IsBeingDeleted() bool { + return !s.ObjectMeta.DeletionTimestamp.IsZero() +} + +func (s *SharedPolicyGroup) GetSpec() core.Spec { + return &s.Spec +} + +func (s *SharedPolicyGroup) GetStatus() core.Status { + return &s.Status +} + +func (s *SharedPolicyGroup) GetRef() core.ObjectRef { + return &refs.NamespacedName{ + Name: s.Name, + Namespace: s.Namespace, + } +} + +func (s *SharedPolicyGroup) ContextRef() core.ObjectRef { + return s.Spec.Context +} + +func (s *SharedPolicyGroup) HasContext() bool { + return s.Spec.Context != nil +} + +func (s *SharedPolicyGroup) GetID() string { + return s.Status.CrossID +} + +func (s *SharedPolicyGroup) PopulateIDs(context core.ContextModel) { + if s.Spec.CrossID != nil { + s.Spec.CrossID = &s.Status.CrossID + } +} + +func (s *SharedPolicyGroup) GetOrgID() string { + return s.Status.OrgID +} + +func (s *SharedPolicyGroup) GetEnvID() string { + return s.Status.EnvID +} + +func (s *SharedPolicyGroupSpecStatus) SetProcessingStatus(status core.ProcessingStatus) { + s.Status.ProcessingStatus = status +} + +func (s *SharedPolicyGroupSpecStatus) IsFailed() bool { + return s.ProcessingStatus == core.ProcessingStatusFailed +} + +func (s *SharedPolicyGroupSpecStatus) DeepCopyFrom(obj client.Object) error { + switch t := obj.(type) { + case *SharedPolicyGroup: + t.Status.DeepCopyInto(s) + return nil + default: + return fmt.Errorf("unknown type %T", t) + } +} + +func (s *SharedPolicyGroupSpecStatus) DeepCopyTo(obj client.Object) error { + switch t := obj.(type) { + case *SharedPolicyGroup: + s.DeepCopyInto(&t.Status) + return nil + default: + return fmt.Errorf("unknown type %T", t) + } +} + +func init() { + SchemeBuilder.Register(&SharedPolicyGroup{}, &SharedPolicyGroupList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7adea3015..446fac901 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1alpha1 import ( "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" "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" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -503,6 +504,106 @@ func (in *ManagementContextStatus) DeepCopy() *ManagementContextStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SharedPolicyGroup) DeepCopyInto(out *SharedPolicyGroup) { + *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 SharedPolicyGroup. +func (in *SharedPolicyGroup) DeepCopy() *SharedPolicyGroup { + if in == nil { + return nil + } + out := new(SharedPolicyGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SharedPolicyGroup) 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 *SharedPolicyGroupList) DeepCopyInto(out *SharedPolicyGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SharedPolicyGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharedPolicyGroupList. +func (in *SharedPolicyGroupList) DeepCopy() *SharedPolicyGroupList { + if in == nil { + return nil + } + out := new(SharedPolicyGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SharedPolicyGroupList) 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 *SharedPolicyGroupSpec) DeepCopyInto(out *SharedPolicyGroupSpec) { + *out = *in + if in.SharedPolicyGroup != nil { + in, out := &in.SharedPolicyGroup, &out.SharedPolicyGroup + *out = new(policygroups.SharedPolicyGroup) + (*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 SharedPolicyGroupSpec. +func (in *SharedPolicyGroupSpec) DeepCopy() *SharedPolicyGroupSpec { + if in == nil { + return nil + } + out := new(SharedPolicyGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SharedPolicyGroupSpecStatus) DeepCopyInto(out *SharedPolicyGroupSpecStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharedPolicyGroupSpecStatus. +func (in *SharedPolicyGroupSpecStatus) DeepCopy() *SharedPolicyGroupSpecStatus { + if in == nil { + return nil + } + out := new(SharedPolicyGroupSpecStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Subscription) DeepCopyInto(out *Subscription) { *out = *in diff --git a/controllers/apim/policygroups/internal/delete.go b/controllers/apim/policygroups/internal/delete.go new file mode 100644 index 000000000..e79dcc157 --- /dev/null +++ b/controllers/apim/policygroups/internal/delete.go @@ -0,0 +1,55 @@ +// 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" + "fmt" + + "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" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/uuid" + util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func Delete( + ctx context.Context, + spg *v1alpha1.SharedPolicyGroup, +) error { + if !util.ContainsFinalizer(spg, core.SharedPolicyGroupFinalizer) { + return nil + } + + apim, apimErr := apim.FromContextRef(ctx, spg.Spec.Context, spg.GetNamespace()) + if apimErr != nil { + return apimErr + } + + if spg.Status.CrossID == "" { + return fmt.Errorf("can not delete a CRD that hasn't been successfuly created in APIM") + } + + // We generate the ID in the same way in the Java side + // This can prevent recreating the same Object multiple times + id := uuid.JavaUUIDFromBytes([]byte(fmt.Sprintf("%s-%s", *spg.Spec.Name, spg.Status.EnvID))) + + if err := apim.SharedPolicyGroup.Delete(id); errors.IgnoreNotFound(err) != nil { + return err + } + + return nil +} diff --git a/controllers/apim/policygroups/internal/status.go b/controllers/apim/policygroups/internal/status.go new file mode 100644 index 000000000..38afa72d7 --- /dev/null +++ b/controllers/apim/policygroups/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, sharedPolicyGroup *v1alpha1.SharedPolicyGroup) error { + if sharedPolicyGroup.IsBeingDeleted() { + return nil + } + + sharedPolicyGroup.Status.ProcessingStatus = core.ProcessingStatusCompleted + return k8s.GetClient().Status().Update(ctx, sharedPolicyGroup) +} + +func UpdateStatusFailure(ctx context.Context, sharedPolicyGroup *v1alpha1.SharedPolicyGroup) error { + sharedPolicyGroup.Status.ProcessingStatus = core.ProcessingStatusFailed + return k8s.GetClient().Status().Update(ctx, sharedPolicyGroup) +} diff --git a/controllers/apim/policygroups/internal/update.go b/controllers/apim/policygroups/internal/update.go new file mode 100644 index 000000000..3c97699c8 --- /dev/null +++ b/controllers/apim/policygroups/internal/update.go @@ -0,0 +1,42 @@ +// 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/errors" +) + +func CreateOrUpdate(ctx context.Context, spg *v1alpha1.SharedPolicyGroup) error { + spec := &spg.Spec + + apim, err := apim.FromContextRef(ctx, spec.Context, spg.GetNamespace()) + if err != nil { + return err + } + + spg.PopulateIDs(apim.Context) + + status, mgmtErr := apim.SharedPolicyGroup.CreateOrUpdate(spec.SharedPolicyGroup) + if mgmtErr != nil { + return errors.NewContextError(mgmtErr) + } + + status.DeepCopyInto(&spg.Status.Status) + return nil +} diff --git a/controllers/apim/policygroups/sharedpolicygroups_controller.go b/controllers/apim/policygroups/sharedpolicygroups_controller.go new file mode 100644 index 000000000..6668ecce5 --- /dev/null +++ b/controllers/apim/policygroups/sharedpolicygroups_controller.go @@ -0,0 +1,124 @@ +// 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 policygroups + +import ( + "context" + "time" + + "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/policygroups/internal" + + "github.com/go-logr/logr" + "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/template" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/predicate" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/watch" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" +) + +const requeueAfterTime = time.Second * 5 + +type Reconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Recorder record.EventRecorder + Watcher watch.Interface +} + +// Reconcile will handle v1alpha1.SharedPolicyGroup CRDs +// +kubebuilder:rbac:groups=gravitee.io,resources=sharedpolicygroups,verbs=get;list;watch;create;update;patch;delete;deletecollection +// +kubebuilder:rbac:groups=gravitee.io,resources=sharedpolicygroups/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=gravitee.io,resources=sharedpolicygroups/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + spg := &v1alpha1.SharedPolicyGroup{} + if err := r.Get(ctx, req.NamespacedName, spg); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + spg.SetNamespace(req.Namespace) + + events := event.NewRecorder(r.Recorder) + + dc := spg.DeepCopy() + + _, err := util.CreateOrUpdate(ctx, r.Client, spg, func() error { + util.AddFinalizer(spg, core.SharedPolicyGroupFinalizer) + k8s.AddAnnotation(spg, core.LastSpecHashAnnotation, hash.Calculate(&spg.Spec)) + + if err := template.Compile(ctx, dc); err != nil { + spg.Status.ProcessingStatus = core.ProcessingStatusFailed + return err + } + + var err error + if spg.IsBeingDeleted() { + err = events.Record(event.Delete, spg, func() error { + if err := internal.Delete(ctx, dc); err != nil { + return err + } + util.RemoveFinalizer(spg, core.SharedPolicyGroupFinalizer) + return nil + }) + } else { + err = events.Record(event.Update, spg, func() error { + return internal.CreateOrUpdate(ctx, dc) + }) + } + + return err + }) + + if err := dc.GetStatus().DeepCopyTo(spg); err != nil { + return ctrl.Result{}, err + } + + if err == nil { + log.InfoEndReconcile(ctx, spg) + return ctrl.Result{}, internal.UpdateStatusSuccess(ctx, spg) + } + + // An error occurred during the reconcile + if err := internal.UpdateStatusFailure(ctx, spg); err != nil { + return ctrl.Result{}, err + } + + if errors.IsRecoverable(err) { + log.ErrorRequeuingReconcile(ctx, err, spg) + return ctrl.Result{RequeueAfter: requeueAfterTime}, err + } + + log.ErrorAbortingReconcile(ctx, err, spg) + return ctrl.Result{}, nil +} + +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.SharedPolicyGroup{}). + WithEventFilter(predicate.LastSpecHashPredicate{}). + Complete(r) +} diff --git a/crdoc/toc.yaml b/crdoc/toc.yaml index 605bad160..a0a5fd275 100644 --- a/crdoc/toc.yaml +++ b/crdoc/toc.yaml @@ -26,3 +26,4 @@ groups: - name: ApiResource - name: Application - name: Subscription + - name: SharedPolicyGroup diff --git a/docs/api/reference.md b/docs/api/reference.md index 4a2fa4f5b..05a815139 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -50,6 +50,10 @@ Resources Subscription + + + SharedPolicyGroup + SharedPolicyGroup @@ -8821,3 +8825,321 @@ when the API gets synced with APIM
false + +## SharedPolicyGroup + +[gravitee.io/v1alpha1](#graviteeiov1alpha1) + + + + + + +SharedPolicyGroup + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
specobject + SharedPolicyGroupSpec
+
false
statusobject + SharedPolicyGroupSpecStatus defines the observed state of an API Context.
+
false
+ + +### SharedPolicyGroup.spec +[Go to parent definition](#sharedpolicygroup) + + + +SharedPolicyGroupSpec + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiTypeenum + Specify the SharedPolicyGroup ApiType
+
+ Enum: MESSAGE, PROXY, NATIVE
+
true
contextRefobject +
+
true
namestring + SharedPolicyGroup name
+
true
phaseenum + SharedPolicyGroup phase (REQUEST;RESPONSE;INTERACT;CONNECT;PUBLISH;SUBSCRIBE)
+
+ Enum: REQUEST, RESPONSE, INTERACT, CONNECT, PUBLISH, SUBSCRIBE
+
true
crossIdstring + CrossID to export SharedPolicyGroup into different environments
+
false
descriptionstring + SharedPolicyGroup description
+
false
prerequisiteMessagestring + SharedPolicyGroup prerequisite Message
+
false
steps[]object + SharedPolicyGroup Steps
+
false
+ + +### SharedPolicyGroup.spec.contextRef +[Go to parent definition](#sharedpolicygroupspec) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
namestring +
+
true
kindstring +
+
false
namespacestring +
+
false
+ + +### SharedPolicyGroup.spec.steps[index] +[Go to parent definition](#sharedpolicygroupspec) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
enabledboolean + Indicate if this FlowStep is enabled or not
+
+ Default: true
+
true
conditionstring + FlowStep condition
+
false
configurationobject + FlowStep configuration is a map of arbitrary key-values
+
false
descriptionstring + FlowStep description
+
false
namestring + FlowStep name
+
false
policystring + FlowStep policy
+
false
+ + +### SharedPolicyGroup.status +[Go to parent definition](#sharedpolicygroup) + + + +SharedPolicyGroupSpecStatus defines the observed state of an API Context. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
crossIdstring + The Cross ID is used to identify an SharedPolicyGroup that has been promoted from one environment to another.
+
false
environmentIdstring + The environment ID, if a management context has been defined to sync with an APIM instance
+
false
errorsobject + When SharedPolicyGroup has been created regardless of errors, this field is +used to persist the error message encountered during admission
+
false
organizationIdstring + The organization ID, if a management context has been defined to sync with an APIM instance
+
false
processingStatusstring + The processing status of the SharedPolicyGroup. +The value is `Completed` if the sync with APIM succeeded, Failed otherwise.
+
false
+ + +### SharedPolicyGroup.status.errors +[Go to parent definition](#sharedpolicygroupstatus) + + + +When SharedPolicyGroup has been created regardless of errors, this field is +used to persist the error message encountered during admission + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
severe[]string + 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
+
false
warning[]string + 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
+
false
diff --git a/examples/apim/shared_policy_groups/shared_policy_groups.yml b/examples/apim/shared_policy_groups/shared_policy_groups.yml new file mode 100644 index 000000000..8f9642dfc --- /dev/null +++ b/examples/apim/shared_policy_groups/shared_policy_groups.yml @@ -0,0 +1,26 @@ +# +# 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: SharedPolicyGroup +metadata: + name: simple-shared-policy-groups +spec: + contextRef: + name: "dev-ctx" + name: "simple-shared-policy-groups" + description: "Simple shared policy groups" + apiType: "PROXY" + phase: "REQUEST" \ No newline at end of file diff --git a/helm/gko/crds/gravitee.io_sharedpolicygroups.yaml b/helm/gko/crds/gravitee.io_sharedpolicygroups.yaml new file mode 100644 index 000000000..7df27b2a0 --- /dev/null +++ b/helm/gko/crds/gravitee.io_sharedpolicygroups.yaml @@ -0,0 +1,190 @@ +# 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: sharedpolicygroups.gravitee.io +spec: + group: gravitee.io + names: + kind: SharedPolicyGroup + listKind: SharedPolicyGroupList + plural: sharedpolicygroups + shortNames: + - sharedpolicygroups + singular: sharedpolicygroup + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: name + type: string + - jsonPath: .spec.description + name: description + type: string + - jsonPath: .spec.apiType + name: apiType + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: SharedPolicyGroup + 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: + description: SharedPolicyGroupSpec + properties: + apiType: + description: Specify the SharedPolicyGroup ApiType + enum: + - MESSAGE + - PROXY + - NATIVE + type: string + contextRef: + properties: + kind: + type: string + name: + type: string + namespace: + type: string + required: + - name + type: object + crossId: + description: CrossID to export SharedPolicyGroup into different environments + type: string + description: + description: SharedPolicyGroup description + type: string + name: + description: SharedPolicyGroup name + type: string + phase: + description: SharedPolicyGroup phase (REQUEST;RESPONSE;INTERACT;CONNECT;PUBLISH;SUBSCRIBE) + enum: + - REQUEST + - RESPONSE + - INTERACT + - CONNECT + - PUBLISH + - SUBSCRIBE + type: string + prerequisiteMessage: + description: SharedPolicyGroup prerequisite Message + type: string + steps: + description: SharedPolicyGroup Steps + items: + properties: + condition: + description: FlowStep condition + type: string + configuration: + description: FlowStep configuration is a map of arbitrary key-values + type: object + x-kubernetes-preserve-unknown-fields: true + description: + description: FlowStep description + type: string + enabled: + default: true + description: Indicate if this FlowStep is enabled or not + type: boolean + name: + description: FlowStep name + type: string + policy: + description: FlowStep policy + type: string + required: + - enabled + type: object + type: array + required: + - apiType + - contextRef + - name + - phase + type: object + status: + description: SharedPolicyGroupSpecStatus defines the observed state of + an API Context. + properties: + crossId: + description: The Cross ID is used to identify an SharedPolicyGroup + that has been promoted from one environment to another. + type: string + environmentId: + description: The environment ID, if a management context has been + defined to sync with an APIM instance + type: string + errors: + description: |- + When SharedPolicyGroup 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 + organizationId: + description: The organization ID, if a management context has been + defined to sync with an APIM instance + type: string + processingStatus: + description: |- + The processing status of the SharedPolicyGroup. + The value is `Completed` if the sync with APIM succeeded, Failed otherwise. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/admission/policygroups/ctrl.go b/internal/admission/policygroups/ctrl.go new file mode 100644 index 000000000..34d4dcb1c --- /dev/null +++ b/internal/admission/policygroups/ctrl.go @@ -0,0 +1,68 @@ +// 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 policygroups + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + ctrl "sigs.k8s.io/controller-runtime" +) + +var _ webhook.CustomValidator = AdmissionCtrl{} +var _ webhook.CustomDefaulter = AdmissionCtrl{} + +func (a AdmissionCtrl) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha1.SharedPolicyGroup{}). + WithValidator(a). + WithDefaulter(a). + Complete() +} + +type AdmissionCtrl struct{} + +// Default implements admission.CustomDefaulter. +func (a AdmissionCtrl) Default(ctx context.Context, obj runtime.Object) error { + return nil +} + +// ValidateCreate implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateCreate( + ctx context.Context, + obj runtime.Object, +) (admission.Warnings, error) { + return validateCreate(ctx, obj).Map() +} + +// ValidateDelete implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateDelete( + ctx context.Context, obj runtime.Object, +) (admission.Warnings, error) { + return validateDelete(ctx, obj).Map() +} + +// ValidateUpdate implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateUpdate( + ctx context.Context, + oldObj runtime.Object, + newObj runtime.Object, +) (admission.Warnings, error) { + return validateUpdate(ctx, oldObj, newObj).Map() +} diff --git a/internal/admission/policygroups/validate.go b/internal/admission/policygroups/validate.go new file mode 100644 index 000000000..e19c98841 --- /dev/null +++ b/internal/admission/policygroups/validate.go @@ -0,0 +1,107 @@ +// 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 policygroups + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission" + + "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/core" + "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 spg, ok := obj.(core.SharedPolicyGroupObject); ok { + // Should be the first validation, it will also compile the templates internally + errs.Add(admission.CompileAndValidateTemplate(ctx, spg)) + if errs.IsSevere() { + return errs + } + errs.Add(ctxref.Validate(ctx, spg)) + if errs.IsSevere() { + return errs + } + errs.MergeWith(validateDryRun(ctx, spg)) + } + return errs +} + +func validateUpdate( + ctx context.Context, + oldObj runtime.Object, + newObj runtime.Object, +) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + _, ook := oldObj.(core.SharedPolicyGroupObject) + newSgp, nok := newObj.(core.SharedPolicyGroupObject) + if ook && nok { + // Should be the first validation, it will also compile the templates internally + errs.Add(admission.CompileAndValidateTemplate(ctx, newSgp)) + if errs.IsSevere() { + return errs + } + errs.Add(ctxref.Validate(ctx, newSgp)) + if errs.IsSevere() { + return errs + } + errs.MergeWith(validateDryRun(ctx, newSgp)) + } + return errs +} + +func validateDryRun(ctx context.Context, spg core.SharedPolicyGroupObject) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + + cp, _ := spg.DeepCopyObject().(core.SharedPolicyGroupObject) + + apim, err := apim.FromContextRef(ctx, cp.ContextRef(), cp.GetNamespace()) + if err != nil { + errs.AddSevere(err.Error()) + } + + cp.PopulateIDs(apim.Context) + + impl, ok := cp.GetSpec().(*v1alpha1.SharedPolicyGroupSpec) + if !ok { + errs.AddSeveref("unable to call dry run (unknown type %T)", impl) + } + + status, err := apim.SharedPolicyGroup.DryRunCreateOrUpdate(impl.SharedPolicyGroup) + if err != nil { + errs.AddSevere(err.Error()) + return errs + } + for _, severe := range status.Errors.Severe { + errs.AddSevere(severe) + } + if errs.IsSevere() { + return errs + } + for _, warning := range status.Errors.Warning { + errs.AddWarning(warning) + } + return errs +} + +func validateDelete(_ context.Context, _ runtime.Object) *errors.AdmissionErrors { + return nil +} diff --git a/internal/apim/apim.go b/internal/apim/apim.go index 050018f9f..332c069aa 100644 --- a/internal/apim/apim.go +++ b/internal/apim/apim.go @@ -31,10 +31,11 @@ const ( // APIM wraps services needed to sync resources with a given environment on a Gravitee.io APIM instance. type APIM struct { - APIs *service.APIs - Applications *service.Applications - Subscription *service.Subscriptions - Env *service.Env + APIs *service.APIs + Applications *service.Applications + Subscription *service.Subscriptions + SharedPolicyGroup *service.SharedPolicyGroup + Env *service.Env Context core.ContextModel } @@ -69,11 +70,12 @@ func FromContext(ctx context.Context, context core.ContextModel, parentNs string } return &APIM{ - APIs: service.NewAPIs(client), - Applications: service.NewApplications(client), - Subscription: service.NewSubscriptions(client), - Env: service.NewEnv(client), - Context: context, + APIs: service.NewAPIs(client), + Applications: service.NewApplications(client), + Subscription: service.NewSubscriptions(client), + SharedPolicyGroup: service.NewSharedPolicyGroup(client), + Env: service.NewEnv(client), + Context: context, }, nil } diff --git a/internal/apim/model/sharedpolicygroups.go b/internal/apim/model/sharedpolicygroups.go new file mode 100644 index 000000000..4fa9e3e00 --- /dev/null +++ b/internal/apim/model/sharedpolicygroups.go @@ -0,0 +1,34 @@ +// 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 model + +import "time" + +type SharedPolicyGroup struct { + ID string `json:"id,omitempty"` + CrossID string `json:"crossId,omitempty"` + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Description string `json:"description,omitempty"` + PrerequisiteMessage string `json:"prerequisiteMessage,omitempty"` + Version int `json:"version,omitempty"` + AppType string `json:"type,omitempty"` + Steps []any `json:"steps,omitempty"` // it is not really important for us + Phase string `json:"phase,omitempty"` + DeployedAt time.Time `json:"deployedAt,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + LifecycleState string `json:"lifecycleState,omitempty"` +} diff --git a/internal/apim/service/sharedpolicygroups.go b/internal/apim/service/sharedpolicygroups.go new file mode 100644 index 000000000..0d6b5f6bc --- /dev/null +++ b/internal/apim/service/sharedpolicygroups.go @@ -0,0 +1,101 @@ +// 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 service + +import ( + "strconv" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/policygroups" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/client" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" +) + +const ( + sharedPolicyGroupsPath = "/shared-policy-groups" +) + +// SharedPolicyGroup brings support for managing gravitee.io APIM SharedPolicyGroups. +type SharedPolicyGroup struct { + *client.Client +} + +func NewSharedPolicyGroup(client *client.Client) *SharedPolicyGroup { + return &SharedPolicyGroup{Client: client} +} + +// Search For tests purposes only. +func (svc *SharedPolicyGroup) Search(query string, status string) ([]model.SharedPolicyGroup, error) { + url := svc.EnvV2Target(sharedPolicyGroupsPath).WithQueryParam("q", query).WithQueryParam("status", status) + sgp := new([]model.SharedPolicyGroup) + + if err := svc.HTTP.Get(url.String(), sgp); err != nil { + return nil, err + } + + return *sgp, nil +} + +// GetByID For tests purposes only. +func (svc *SharedPolicyGroup) GetByID(id string) (*model.SharedPolicyGroup, error) { + if id == "" { + return nil, errors.NewNotFoundError() + } + + url := svc.EnvV2Target(sharedPolicyGroupsPath).WithPath(id) + sgp := new(model.SharedPolicyGroup) + + if err := svc.HTTP.Get(url.String(), sgp); err != nil { + return nil, err + } + + return sgp, nil +} + +func (svc *SharedPolicyGroup) CreateOrUpdate(spec *policygroups.SharedPolicyGroup) (*policygroups.Status, error) { + return svc.createOrUpdate(spec, false) +} + +func (svc *SharedPolicyGroup) DryRunCreateOrUpdate(spec *policygroups.SharedPolicyGroup) (*policygroups.Status, error) { + return svc.createOrUpdate(spec, true) +} + +func (svc *SharedPolicyGroup) createOrUpdate(spec *policygroups.SharedPolicyGroup, + dryRun bool) (*policygroups.Status, error) { + url := svc.EnvV2Target(sharedPolicyGroupsPath). + WithPath("_import/crd"). + WithQueryParam("dryRun", strconv.FormatBool(dryRun)) + + status := new(policygroups.Status) + apimSpg := struct { + *policygroups.SharedPolicyGroup + OriginContext string `json:"originContext"` + }{ + SharedPolicyGroup: spec, + OriginContext: "KUBERNETES", + } + + if err := svc.HTTP.Put(url.String(), apimSpg, status); err != nil { + return nil, err + } + + return status, nil +} + +func (svc *SharedPolicyGroup) Delete(id string) error { + url := svc.EnvV2Target(sharedPolicyGroupsPath).WithPath(id) + return svc.HTTP.Delete(url.String(), nil) +} diff --git a/internal/core/interface.go b/internal/core/interface.go index 66ceaf492..25e688e75 100644 --- a/internal/core/interface.go +++ b/internal/core/interface.go @@ -130,6 +130,11 @@ type SubscribableStatus interface { RemoveSubscription() } +// +k8s:deepcopy-gen=false +type SharedPolicyGroupObject interface { + ContextAwareObject +} + // +k8s:deepcopy-gen=false type ContextAwareObject interface { Object diff --git a/internal/core/keys.go b/internal/core/keys.go index 1a0efae98..8da44ff13 100644 --- a/internal/core/keys.go +++ b/internal/core/keys.go @@ -46,6 +46,7 @@ const ( ApplicationFinalizer = "finalizers.gravitee.io/applicationdeletion" SubscriptionFinalizer = "finalizers.gravitee.io/subscriptions" TemplatingFinalizer = "finalizers.gravitee.io/templating" + SharedPolicyGroupFinalizer = "finalizers.gravitee.io/sharedpolicygroups" CloudTokenSecretKey = "cloudToken" BearerTokenSecretKey = "bearerToken" diff --git a/internal/log/log.go b/internal/log/log.go index a00a084da..0f7112ad5 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -90,7 +90,7 @@ func InfoInitReconcile(ctx context.Context, obj client.Object) { func InfoEndReconcile(ctx context.Context, obj client.Object) { log.FromContext(ctx).Info( fmt.Sprintf( - "Resource [%s] as been successfully reconciled", + "Resource [%s] has been successfully reconciled", obj.GetObjectKind().GroupVersionKind().GroupKind(), ), KeyValues(obj)..., diff --git a/internal/predicate/predicate.go b/internal/predicate/predicate.go index f1f1713b2..2801bc877 100644 --- a/internal/predicate/predicate.go +++ b/internal/predicate/predicate.go @@ -51,6 +51,8 @@ func (LastSpecHashPredicate) Create(e event.CreateEvent) bool { t.Status.ProcessingStatus != core.ProcessingStatusCompleted case *v1alpha1.Subscription: return e.Object.GetAnnotations()[core.LastSpecHashAnnotation] != hash.Calculate(&t.Spec) + case *v1alpha1.SharedPolicyGroup: + 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: @@ -89,6 +91,9 @@ func (LastSpecHashPredicate) Update(e event.UpdateEvent) bool { case *v1alpha1.Subscription: oo, _ := e.ObjectOld.(*v1alpha1.Subscription) return hash.Calculate(&no.Spec) != hash.Calculate(&oo.Spec) + case *v1alpha1.SharedPolicyGroup: + oo, _ := e.ObjectOld.(*v1alpha1.SharedPolicyGroup) + 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/internal/uuid/uuid.go b/internal/uuid/uuid.go index eba281358..c78b24b42 100644 --- a/internal/uuid/uuid.go +++ b/internal/uuid/uuid.go @@ -15,6 +15,8 @@ package uuid import ( + "crypto/md5" //nolint:gosec // it is expected in this context + "fmt" "strings" "github.com/google/uuid" @@ -31,6 +33,31 @@ func FromStrings(seeds ...string) string { return guid.String() } +// JavaUUIDFromBytes generates a version 3 UUID based on the MD5 hash of the provided name, same as java.util.UUID. +func JavaUUIDFromBytes(data []byte) string { + // Calculate the MD5 hash of the input name + h := md5.New() //nolint:gosec // it is expected in this context + h.Reset() + h.Write(data) + md5Bytes := h.Sum(nil) + + // Set version to 3 in the 7th byte (6th byte in zero-index). + md5Bytes[6] &= 0x0f // clear the version bits + md5Bytes[6] |= 0x30 // set to version 3 + + // Set variant to IETF (RFC 4122) in the 9th byte (8th byte in zero-index). + md5Bytes[8] &= 0x3f // clear the variant bits + md5Bytes[8] |= 0x80 // set to IETF variant + + // Convert the byte array to a UUID string representation + return fmt.Sprintf("%x-%x-%x-%x-%x", + md5Bytes[0:4], + md5Bytes[4:6], + md5Bytes[6:8], + md5Bytes[8:10], + md5Bytes[10:]) +} + func NewV4String() string { return uuid.NewString() } diff --git a/main.go b/main.go index 88d05c3bc..6fc39faba 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,8 @@ import ( "os" "strings" + "github.com/gravitee-io/gravitee-kubernetes-operator/controllers/apim/policygroups" + 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" @@ -258,6 +260,15 @@ func registerControllers(mgr manager.Manager) { os.Exit(1) } + if err := (&policygroups.Reconciler{ + Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("sharedpolicygroups-controller"), + }).SetupWithManager(mgr); err != nil { + log.Global.Error(err, "Unable to create controller for shared policy groups") + os.Exit(1) + } + if err := (&secrets.Reconciler{ Client: k8s.GetClient(), Scheme: mgr.GetScheme(),