diff --git a/.golangci.yml b/.golangci.yml index 9c6eab961..e8495d538 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -253,7 +253,7 @@ issues: max-same-issues: 50 exclude: - - ".*(Id|Api|Url|Http).* should be .*(ID|API|URL|HTTP).*" + - ".*(Api|Url|Http).* should be .*(API|URL|HTTP).*" exclude-rules: - source: "^//\\s*go:generate\\s" diff --git a/api/model/api/base/member.go b/api/model/api/base/member.go index f69caeeca..45e1d843f 100644 --- a/api/model/api/base/member.go +++ b/api/model/api/base/member.go @@ -22,7 +22,7 @@ type Member struct { // Member source ID // +kubebuilder:validation:Required // +kubebuilder:example:=user@email.com - SourceId string `json:"sourceId"` + SourceID string `json:"sourceId"` // Member display name DisplayName string `json:"displayName,omitempty"` // The API role associated with this Member @@ -33,7 +33,7 @@ type Member struct { func NewGraviteeMember(username, role string) *Member { return &Member{ Source: "gravitee", - SourceId: username, + SourceID: username, Role: role, } } @@ -41,7 +41,7 @@ func NewGraviteeMember(username, role string) *Member { func NewMemoryMember(username, role string) *Member { return &Member{ Source: "memory", - SourceId: username, + SourceID: username, Role: role, } } diff --git a/api/model/api/base/page.go b/api/model/api/base/page.go index 75ab13787..96228128b 100644 --- a/api/model/api/base/page.go +++ b/api/model/api/base/page.go @@ -26,7 +26,7 @@ type PageSource struct { type AccessControl struct { // +kubebuilder:validation:Required // The ID denied or granted by the access control (currently only group names are supported) - ReferenceId string `json:"referenceId,omitempty"` + ReferenceID string `json:"referenceId,omitempty"` // +kubebuilder:validation:Required // +kubebuilder:validation:Enum=GROUP; // The type of reference denied or granted by the access control diff --git a/api/model/api/base/plan.go b/api/model/api/base/plan.go index f5b49f2cf..3182da149 100644 --- a/api/model/api/base/plan.go +++ b/api/model/api/base/plan.go @@ -30,11 +30,11 @@ type PlanValidation string type Plan struct { // Plan ID - Id string `json:"id,omitempty"` + ID string `json:"id,omitempty"` // The plan Cross ID. // This field is used to identify plans defined for an API // that has been promoted between different environments. - CrossId string `json:"crossId,omitempty"` + CrossID string `json:"crossId,omitempty"` // Plan Description Description string `json:"description"` // List of plan tags @@ -71,11 +71,11 @@ func (plan *Plan) WithStatus(status PlanStatus) *Plan { } func (plan *Plan) WithID(id string) *Plan { - plan.Id = id + plan.ID = id return plan } func (plan *Plan) WithCrossID(id string) *Plan { - plan.CrossId = id + plan.CrossID = id return plan } diff --git a/api/model/api/base/primary_owner.go b/api/model/api/base/primary_owner.go index 1b616fe0f..7593ace7d 100644 --- a/api/model/api/base/primary_owner.go +++ b/api/model/api/base/primary_owner.go @@ -17,7 +17,7 @@ package base type PrimaryOwner struct { // +kubebuilder:validation:Required // PrimaryOwner ID - Id string `json:"id,omitempty"` + ID string `json:"id,omitempty"` // +kubebuilder:validation:Optional // PrimaryOwner email Email string `json:"email,omitempty"` diff --git a/api/model/api/v2/api.go b/api/model/api/v2/api.go index 653945e5e..ea601c8e3 100644 --- a/api/model/api/v2/api.go +++ b/api/model/api/v2/api.go @@ -17,8 +17,11 @@ package v2 import ( "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" ) +var _ custom.ApiDefinition = &Api{} + type Api struct { *base.ApiBase `json:",inline"` // +kubebuilder:validation:Required @@ -69,6 +72,15 @@ type Api struct { ExecutionMode string `json:"execution_mode,omitempty"` } +func (api *Api) GetDefinitionVersion() custom.ApiDefinitionVersion { + return custom.ApiV2 +} + +// TODO implement when v2 admission handles paths +func (api *Api) GetContextPaths() ([]string, error) { + return make([]string, 0), nil +} + const ( ModeFullyManaged = "fully_managed" OriginKubernetes = "kubernetes" diff --git a/api/model/api/v2/plan.go b/api/model/api/v2/plan.go index ee583f69f..933f2d3f7 100644 --- a/api/model/api/v2/plan.go +++ b/api/model/api/v2/plan.go @@ -28,7 +28,7 @@ type Consumer struct { // Consumer type (possible values TAG) ConsumerType ConsumerType `json:"consumerType,omitempty"` // Consumer ID - ConsumerId string `json:"consumerId,omitempty"` + ConsumerID string `json:"consumerId,omitempty"` } type Plan struct { diff --git a/api/model/api/v4/api.go b/api/model/api/v4/api.go index d50e6cb62..3e7b55d78 100644 --- a/api/model/api/v4/api.go +++ b/api/model/api/v4/api.go @@ -16,7 +16,10 @@ package v4 import ( + "fmt" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" ) // +kubebuilder:validation:Enum=PROXY;MESSAGE; @@ -25,6 +28,8 @@ type ApiType string // +kubebuilder:validation:Enum=PUBLISHED;UNPUBLISHED; type ApiV4LifecycleState string +var _ custom.ApiDefinition = &Api{} + type Api struct { *base.ApiBase `json:",inline"` // +kubebuilder:default:=`V4` @@ -185,3 +190,43 @@ func (api *Api) getGatewayDefinitionEndpointGroups() []*EndpointGroup { } return endpointGroups } + +func (api *Api) GetDefinitionVersion() custom.ApiDefinitionVersion { + return custom.ApiV4 +} + +func (api *Api) GetContextPaths() ([]string, error) { + paths := make([]string, 0) + for _, l := range api.Listeners { + paths = append(paths, parseListener(l)...) + } + return paths, nil +} + +func parseListener(l Listener) []string { + if l == nil { + return []string{} + } + + switch t := l.(type) { + case *GenericListener: + return parseListener(t.ToListener()) + case *HttpListener: + { + paths := make([]string, 0) + for _, path := range t.Paths { + if path.Host != "" { + p := fmt.Sprintf("%s/%s", path.Host, path.Path) + paths = append(paths, p) + } else { + paths = append(paths, path.Path) + } + } + return paths + } + case *TCPListener: + return t.Hosts + } + + return []string{} +} diff --git a/api/model/api/v4/status.go b/api/model/api/v4/status.go index 6cc164526..6fb1defbb 100644 --- a/api/model/api/v4/status.go +++ b/api/model/api/v4/status.go @@ -14,7 +14,9 @@ package v4 -import "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" +import ( + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" +) type Status struct { base.Status `json:",inline"` @@ -22,4 +24,18 @@ type Status struct { // for the API definition if a management context has been defined // to sync the API with an APIM instance Plans map[string]string `json:"plans,omitempty"` + // When API has been created regardless of errors, this field is + // used to persist the error message encountered during admission + Errors Errors `json:"errors,omitempty"` +} + +type Errors struct { + // 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 + Warning []string `json:"warning,omitempty"` + // 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 + Severe []string `json:"severe,omitempty"` } diff --git a/api/model/api/v4/zz_generated.deepcopy.go b/api/model/api/v4/zz_generated.deepcopy.go index a0639bea3..df800d3d4 100644 --- a/api/model/api/v4/zz_generated.deepcopy.go +++ b/api/model/api/v4/zz_generated.deepcopy.go @@ -432,6 +432,31 @@ func (in *Entrypoint) DeepCopy() *Entrypoint { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Errors) DeepCopyInto(out *Errors) { + *out = *in + if in.Warning != nil { + in, out := &in.Warning, &out.Warning + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Severe != nil { + in, out := &in.Severe, &out.Severe + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Errors. +func (in *Errors) DeepCopy() *Errors { + if in == nil { + return nil + } + out := new(Errors) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Flow) DeepCopyInto(out *Flow) { *out = *in @@ -889,6 +914,7 @@ func (in *Status) DeepCopyInto(out *Status) { (*out)[key] = val } } + in.Errors.DeepCopyInto(&out.Errors) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. diff --git a/api/model/application/application.go b/api/model/application/application.go index 5ce507a47..93711f3a8 100644 --- a/api/model/application/application.go +++ b/api/model/application/application.go @@ -21,15 +21,15 @@ type AppKeyMode string type SimpleSettings struct { // Application Type AppType string `json:"type"` - // ClientId is the client id of the application - ClientId string `json:"client_id,omitempty"` + // ClientID is the client id of the application + ClientID string `json:"client_id,omitempty"` } type OAuthClientSettings struct { // Oauth client application type ApplicationType string `json:"application_type"` // Oauth client id - ClientId string `json:"client_id,omitempty"` + ClientID string `json:"client_id,omitempty"` // Oauth client secret ClientSecret string `json:"client_secret,omitempty"` // Oauth client uri @@ -77,8 +77,8 @@ type Application struct { Description string `json:"description,omitempty"` // Application Type Type string `json:"type,omitempty"` - // The ClientId identifying the application. This field is required when subscribing to an OAUTH2 / JWT plan. - ClientId string `json:"clientId,omitempty"` + // The ClientID identifying the application. This field is required when subscribing to an OAUTH2 / JWT plan. + ClientID string `json:"clientId,omitempty"` // List of application Redirect Uris RedirectUris []string `json:"redirectUris,omitempty"` diff --git a/api/model/management/context.go b/api/model/management/context.go index e3ca30e5d..e8890d6c8 100644 --- a/api/model/management/context.go +++ b/api/model/management/context.go @@ -17,18 +17,23 @@ package management import ( "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" ) +var _ custom.Auth = &Auth{} +var _ custom.BasicAuth = &BasicAuth{} +var _ custom.Context = &Context{} + type Context struct { // The URL of a management API instance // +kubebuilder:validation:Pattern=`^http(s?):\/\/.+$` BaseUrl string `json:"baseUrl"` // An existing organization id targeted by the context on the management API instance. // +kubebuilder:validation:Required - OrgId string `json:"organizationId"` + OrgID string `json:"organizationId"` // An existing environment id targeted by the context within the organization. // +kubebuilder:validation:Required - EnvId string `json:"environmentId"` + EnvID string `json:"environmentId"` // Auth defines the authentication method used to connect to the API Management. // Can be either basic authentication credentials, a bearer token // or a reference to a kubernetes secret holding one of these two configurations. @@ -36,6 +41,31 @@ type Context struct { Auth *Auth `json:"auth"` } +// GetAuth implements custom.Context. +func (c *Context) GetAuth() custom.Auth { + return c.Auth +} + +// GetEnvID implements custom.Context. +func (c *Context) GetEnvID() string { + return c.EnvID +} + +// GetOrgID implements custom.Context. +func (c *Context) GetOrgID() string { + return c.OrgID +} + +// GetSecretRef implements custom.Context. +func (c *Context) GetSecretRef() custom.ResourceRef { + return c.Auth.SecretRef +} + +// GetURL implements custom.Context. +func (c *Context) GetURL() string { + return c.BaseUrl +} + type Auth struct { // The bearer token used to authenticate against the API Management instance // (must be generated from an admin account) @@ -46,6 +76,39 @@ type Auth struct { SecretRef *refs.NamespacedName `json:"secretRef,omitempty"` } +// GetBearerToken implements custom.Auth. +func (in *Auth) GetBearerToken() string { + return in.BearerToken +} + +// HasCredentials implements custom.Auth. +func (in *Auth) HasCredentials() bool { + return in.Credentials != nil +} + +// GetCredentials implements custom.Auth. +func (in *Auth) GetCredentials() custom.BasicAuth { + return in.Credentials +} + +// GetSecretRef implements custom.Auth. +func (in *Auth) GetSecretRef() custom.ResourceRef { + return in.SecretRef +} + +// SetCredentials implements custom.Auth. +func (in *Auth) SetCredentials(username string, password string) { + in.Credentials = &BasicAuth{ + Username: username, + Password: password, + } +} + +// SetToken implements custom.Auth. +func (in *Auth) SetToken(token string) { + in.BearerToken = token +} + type BasicAuth struct { // +kubebuilder:validation:Required Username string `json:"username,omitempty"` @@ -53,6 +116,16 @@ type BasicAuth struct { Password string `json:"password,omitempty"` } +// GetPassword implements custom.BasicAuth. +func (in *BasicAuth) GetPassword() string { + return in.Password +} + +// GetUsername implements custom.BasicAuth. +func (in *BasicAuth) GetUsername() string { + return in.Username +} + func (c *Context) HasAuthentication() bool { return c.Auth != nil } diff --git a/api/v1alpha1/apiv2definition_types.go b/api/v1alpha1/apiv2definition_types.go index a3644de9c..7de2cf61f 100644 --- a/api/v1alpha1/apiv2definition_types.go +++ b/api/v1alpha1/apiv2definition_types.go @@ -29,7 +29,6 @@ import ( "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/uuid" "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" - "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/list" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -61,8 +60,7 @@ type ApiDefinitionStatus struct { base.Status `json:",inline"` } -var _ list.Item = &ApiDefinition{} -var _ custom.ApiDefinition = &ApiDefinition{} +var _ custom.ApiDefinitionResource = &ApiDefinition{} var _ custom.Status = &ApiDefinitionStatus{} var _ custom.Spec = &ApiDefinitionV2Spec{} @@ -83,39 +81,6 @@ type ApiDefinition struct { Status ApiDefinitionStatus `json:"status,omitempty"` } -// PickID returns the ID of the API definition, when a context has been defined at the spec level. -// The ID might be returned from the API status, meaning that the API is already known. -// If the API is unknown, the ID is either given from the spec if given, -// or generated from the API UID and the context key to ensure uniqueness -// in case the API is replicated on a same APIM instance. -func (api *ApiDefinition) PickID() string { - if api.Status.ID != "" { - return api.Status.ID - } - - if api.Spec.ID != "" { - return api.Spec.ID - } - - return string(api.UID) -} - -func (api *ApiDefinition) PickCrossID() string { - if api.Status.CrossID != "" { - return api.Status.CrossID - } - - return api.GetOrGenerateCrossID() -} - -func (api *ApiDefinition) GetOrGenerateCrossID() string { - if api.Spec.CrossID != "" { - return api.Spec.CrossID - } - - return uuid.FromStrings(api.GetNamespacedName().String()) -} - func (api *ApiDefinition) GetNamespacedName() *refs.NamespacedName { return &refs.NamespacedName{Namespace: api.Namespace, Name: api.Name} } @@ -131,19 +96,19 @@ func (spec *ApiDefinitionV2Spec) SetDefinitionContext() { } } -func (api *ApiDefinition) EnvID() string { +func (api *ApiDefinition) GetEnvID() string { return api.Status.EnvID } -func (api *ApiDefinition) ID() string { +func (api *ApiDefinition) GetID() string { return api.Status.ID } -func (api *ApiDefinition) OrgID() string { +func (api *ApiDefinition) GetOrgID() string { return api.Status.OrgID } -func (api *ApiDefinition) Version() custom.ApiDefinitionVersion { +func (api *ApiDefinition) GetDefinitionVersion() custom.ApiDefinitionVersion { return custom.ApiV2 } @@ -167,6 +132,83 @@ func (api *ApiDefinition) HasContext() bool { return api.Spec.Context != nil } +func (api *ApiDefinition) GetContextPaths() ([]string, error) { + return api.Spec.GetContextPaths() +} + +func (api *ApiDefinition) GetDefinition() custom.ApiDefinition { + return &api.Spec.Api +} + +func (api *ApiDefinition) PopulateIDs(_ custom.Context) { + api.Spec.ID = api.pickID() + api.Spec.CrossID = api.pickCrossID() + api.generateEmptyPlanCrossIDs() + api.generatePageIDs() +} + +// For each plan, generate a Cross id from Api id & Plan Name if not defined. +func (api *ApiDefinition) generateEmptyPlanCrossIDs() { + plans := api.Spec.Plans + + for _, plan := range plans { + if plan.CrossID == "" { + plan.CrossID = uuid.FromStrings(api.Spec.ID, separator, plan.Name) + } + } +} + +func (api *ApiDefinition) generatePageIDs() { + spec := &api.Spec + pages := spec.Pages + for name, page := range pages { + page.API = spec.ID + apiName := api.GetNamespacedName().String() + if page.CrossID == "" { + page.CrossID = uuid.FromStrings(apiName, separator, name) + } + if page.ID == "" { + page.ID = uuid.FromStrings(spec.ID, separator, name) + } + if page.Parent != "" { + page.ParentID = uuid.FromStrings(spec.ID, separator, page.Parent) + } + } +} + +// PickID returns the ID of the API definition, when a context has been defined at the spec level. +// The ID might be returned from the API status, meaning that the API is already known. +// If the API is unknown, the ID is either given from the spec if given, +// or generated from the API UID and the context key to ensure uniqueness +// in case the API is replicated on a same APIM instance. +func (api *ApiDefinition) pickID() string { + if api.Status.ID != "" { + return api.Status.ID + } + + if api.Spec.ID != "" { + return api.Spec.ID + } + + return string(api.UID) +} + +func (api *ApiDefinition) pickCrossID() string { + if api.Status.CrossID != "" { + return api.Status.CrossID + } + + return api.getOrGenerateCrossID() +} + +func (api *ApiDefinition) getOrGenerateCrossID() string { + if api.Spec.CrossID != "" { + return api.Spec.CrossID + } + + return uuid.FromStrings(api.GetNamespacedName().String()) +} + func (spec *ApiDefinitionV2Spec) Hash() string { return hash.Calculate(spec) } @@ -201,8 +243,6 @@ func (s *ApiDefinitionStatus) DeepCopyTo(obj client.Object) error { return nil } -var _ list.Interface = &ApiDefinitionList{} - // ApiDefinitionList contains a list of ApiDefinition. // +kubebuilder:object:root=true type ApiDefinitionList struct { @@ -211,14 +251,6 @@ type ApiDefinitionList struct { Items []ApiDefinition `json:"items"` } -func (l *ApiDefinitionList) GetItems() []list.Item { - items := make([]list.Item, len(l.Items)) - for i := range l.Items { - items[i] = &l.Items[i] - } - return items -} - func init() { SchemeBuilder.Register(&ApiDefinition{}, &ApiDefinitionList{}) } diff --git a/api/v1alpha1/apiv2definition_webhook.go b/api/v1alpha1/apiv2definition_webhook.go deleted file mode 100644 index 8d9db5824..000000000 --- a/api/v1alpha1/apiv2definition_webhook.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 ( - commonMutate "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/common/mutate" - runtime "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var _ webhook.Defaulter = &ApiDefinition{} -var _ webhook.Validator = &ApiDefinition{} - -func (api *ApiDefinition) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(api). - Complete() -} - -func (api *ApiDefinition) Default() { - commonMutate.SetDefaults(api) -} - -func (api *ApiDefinition) ValidateCreate() (admission.Warnings, error) { - return admission.Warnings{}, nil -} - -func (api *ApiDefinition) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { - return admission.Warnings{}, nil -} - -func (*ApiDefinition) ValidateDelete() (admission.Warnings, error) { - return admission.Warnings{}, nil -} diff --git a/api/v1alpha1/apiv4definition_types.go b/api/v1alpha1/apiv4definition_types.go index 1b98830c4..ca1732f70 100644 --- a/api/v1alpha1/apiv4definition_types.go +++ b/api/v1alpha1/apiv4definition_types.go @@ -22,15 +22,15 @@ import ( "github.com/gravitee-io/gravitee-kubernetes-operator/internal/hash" v4 "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/v4" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/uuid" "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" - "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/list" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) +const separator = "/" + // ApiV4DefinitionSpec defines the desired state of ApiDefinition. // +kubebuilder:object:generate=true type ApiV4DefinitionSpec struct { @@ -43,7 +43,7 @@ type ApiV4DefinitionStatus struct { v4.Status `json:",inline"` } -var _ custom.ApiDefinition = &ApiV4Definition{} +var _ custom.ApiDefinitionResource = &ApiV4Definition{} var _ custom.Status = &ApiDefinitionStatus{} var _ custom.Spec = &ApiDefinitionV2Spec{} @@ -73,12 +73,19 @@ func (api *ApiV4Definition) IsBeingDeleted() bool { return !api.ObjectMeta.DeletionTimestamp.IsZero() } -// PickID returns the ID of the API definition, when a context has been defined at the spec level. +func (api *ApiV4Definition) PopulateIDs(context custom.Context) { + api.Spec.ID = api.pickID(context) + api.Spec.CrossID = api.pickCrossID() + api.Spec.Pages = api.pickPageIDs() + api.Spec.Plans = api.pickPlanIDs() +} + +// pickID returns the ID of the API definition, when a context has been defined at the spec level. // The ID might be returned from the API status, meaning that the API is already known. // If the API is unknown, the ID is either given from the spec if given, // or generated from the API UID and the context key to ensure uniqueness // in case the API is replicated on a same APIM instance. -func (api *ApiV4Definition) PickID(mCtx *management.Context) string { +func (api *ApiV4Definition) pickID(mCtx custom.Context) string { if api.Status.ID != "" { return api.Status.ID } @@ -88,13 +95,13 @@ func (api *ApiV4Definition) PickID(mCtx *management.Context) string { } if mCtx != nil { - return uuid.FromStrings(api.PickCrossID(), mCtx.OrgId, mCtx.EnvId) + return uuid.FromStrings(api.pickCrossID(), mCtx.GetOrgID(), mCtx.GetEnvID()) } return string(api.UID) } -func (api *ApiV4Definition) PickCrossID() string { +func (api *ApiV4Definition) pickCrossID() string { if api.Status.CrossID != "" { return api.Status.CrossID } @@ -107,33 +114,22 @@ func (api *ApiV4Definition) PickCrossID() string { return uuid.FromStrings(namespacedName.String()) } -func (api *ApiV4Definition) PickPlanIDs() map[string]*v4.Plan { +func (api *ApiV4Definition) pickPlanIDs() map[string]*v4.Plan { plans := make(map[string]*v4.Plan, len(api.Spec.Plans)) for key, plan := range api.Spec.Plans { p := plan.DeepCopy() if id, ok := api.Status.Plans[key]; ok { - p.Id = id - } else if plan.Id == "" { + p.ID = id + } else if plan.ID == "" { namespacedName := api.GetNamespacedName() - p.Id = uuid.FromStrings(namespacedName.String(), key) + p.ID = uuid.FromStrings(namespacedName.String(), key) } plans[key] = p } return plans } -const separator = "/" - -// GetOrGenerateEmptyPlanCrossID For each plan, generate a CrossId from Api Id & Plan Name if not defined. -func (api *ApiV4Definition) GetOrGenerateEmptyPlanCrossID() { - for name, plan := range api.Spec.Plans { - if plan.CrossId == "" { - plan.CrossId = uuid.FromStrings(api.PickCrossID(), separator, name) - } - } -} - -func (api *ApiV4Definition) PickPageIDs() map[string]*v4.Page { +func (api *ApiV4Definition) pickPageIDs() map[string]*v4.Page { pages := make(map[string]*v4.Page, len(api.Spec.Pages)) for name, page := range api.Spec.Pages { p := page.DeepCopy() @@ -155,18 +151,18 @@ func (api *ApiV4Definition) PickPageIDs() map[string]*v4.Page { return pages } -// EnvID implements custom.ApiDefinition. -func (api *ApiV4Definition) EnvID() string { +// GetEnvID implements custom.ApiDefinition. +func (api *ApiV4Definition) GetEnvID() string { return api.Status.EnvID } -// ID implements custom.ApiDefinition. -func (api *ApiV4Definition) ID() string { +// GetID implements custom.ApiDefinition. +func (api *ApiV4Definition) GetID() string { return api.Status.ID } -// OrgID implements custom.ApiDefinition. -func (api *ApiV4Definition) OrgID() string { +// GetOrgID implements custom.ApiDefinition. +func (api *ApiV4Definition) GetOrgID() string { return api.Status.OrgID } @@ -202,6 +198,18 @@ func (api *ApiV4Definition) GetObjectMeta() *metav1.ObjectMeta { return &api.ObjectMeta } +func (api *ApiV4Definition) GetContextPaths() ([]string, error) { + return api.Spec.GetContextPaths() +} + +func (api *ApiV4Definition) GetDefinitionVersion() custom.ApiDefinitionVersion { + return custom.ApiV4 +} + +func (api *ApiV4Definition) GetDefinition() custom.ApiDefinition { + return &api.Spec.Api +} + func (spec *ApiV4DefinitionSpec) Hash() string { return hash.Calculate(spec) } @@ -248,14 +256,6 @@ type ApiV4DefinitionList struct { Items []ApiV4Definition `json:"items"` } -func (l *ApiV4DefinitionList) GetItems() []list.Item { - items := make([]list.Item, len(l.Items)) - for i := range l.Items { - items[i] = &l.Items[i] - } - return items -} - func init() { SchemeBuilder.Register(&ApiV4Definition{}, &ApiV4DefinitionList{}) } diff --git a/api/v1alpha1/apiv4definition_webhook.go b/api/v1alpha1/apiv4definition_webhook.go deleted file mode 100644 index a0826358e..000000000 --- a/api/v1alpha1/apiv4definition_webhook.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 ( - "context" - - commonMutate "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/common/mutate" - wk "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/webhook" - runtime "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var _ webhook.Defaulter = &ApiV4Definition{} -var _ webhook.Validator = &ApiV4Definition{} - -func (api *ApiV4Definition) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(api). - Complete() -} - -func (api *ApiV4Definition) Default() { - commonMutate.SetDefaults(api) -} - -func (api *ApiV4Definition) ValidateCreate() (admission.Warnings, error) { - return wk.ValidateApiV4(context.Background(), &api.Spec.Api, api.Name, api.Namespace, api.Spec.Context) -} - -func (api *ApiV4Definition) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { - return wk.ValidateApiV4(context.Background(), &api.Spec.Api, api.Name, api.Namespace, api.Spec.Context) -} - -func (*ApiV4Definition) ValidateDelete() (admission.Warnings, error) { - return admission.Warnings{}, nil -} diff --git a/api/v1alpha1/application_types.go b/api/v1alpha1/application_types.go index e9ceadc8d..950e09901 100644 --- a/api/v1alpha1/application_types.go +++ b/api/v1alpha1/application_types.go @@ -27,6 +27,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +var _ custom.ContextAwareResource = &Application{} + // Application is the main resource handled by the Kubernetes Operator // +kubebuilder:object:generate=true type ApplicationSpec struct { @@ -87,10 +89,18 @@ func (app *Application) HasContext() bool { return app.Spec.Context != nil } -func (app *Application) ID() string { +func (app *Application) GetID() string { return app.Status.ID } +func (app *Application) GetOrgID() string { + return app.Status.OrgID +} + +func (app *Application) GetEnvID() string { + return app.Status.EnvID +} + func (app *Application) DeepCopyResource() custom.Resource { return app.DeepCopy() } diff --git a/api/v1alpha1/application_webhook.go b/api/v1alpha1/application_webhook.go deleted file mode 100644 index eb350ea25..000000000 --- a/api/v1alpha1/application_webhook.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 ( - commonMutate "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/common/mutate" - runtime "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var _ webhook.Defaulter = &Application{} -var _ webhook.Validator = &Application{} - -func (app *Application) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(app). - Complete() -} - -func (app *Application) Default() { - commonMutate.SetDefaults(app) -} - -func (app *Application) ValidateCreate() (admission.Warnings, error) { - return admission.Warnings{}, nil -} - -func (app *Application) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { - return admission.Warnings{}, nil -} - -func (*Application) ValidateDelete() (admission.Warnings, error) { - return admission.Warnings{}, nil -} diff --git a/api/v1alpha1/managementcontext_types.go b/api/v1alpha1/managementcontext_types.go index 2693d32d1..715274c11 100644 --- a/api/v1alpha1/managementcontext_types.go +++ b/api/v1alpha1/managementcontext_types.go @@ -17,21 +17,67 @@ package v1alpha1 import ( + "fmt" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/hash" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) +var _ custom.ContextResource = &ManagementContext{} +var _ custom.Spec = &ManagementContextSpec{} +var _ custom.Status = &ManagementContextStatus{} + // ManagementContext represents the configuration for a specific environment // +kubebuilder:object:generate=true type ManagementContextSpec struct { *management.Context `json:",inline"` } +// Hash implements custom.Spec. +func (spec *ManagementContextSpec) Hash() string { + return hash.Calculate(spec) +} + // ManagementContextStatus defines the observed state of an API Context. type ManagementContextStatus struct { } +// DeepCopyFrom implements custom.Status. +func (st *ManagementContextStatus) DeepCopyFrom(obj client.Object) error { + switch t := obj.(type) { + case *ManagementContext: + t.Status.DeepCopyInto(st) + return nil + default: + return fmt.Errorf("unknown type %T", t) + } +} + +// DeepCopyTo implements custom.Status. +func (st *ManagementContextStatus) DeepCopyTo(obj client.Object) error { + switch t := obj.(type) { + case *ManagementContext: + st.DeepCopyInto(&t.Status) + return nil + default: + return fmt.Errorf("unknown type %T", t) + } +} + +// SetObservedGeneration implements custom.Status. +func (st *ManagementContextStatus) SetObservedGeneration(g int64) { + // Not implemented +} + +// SetProcessingStatus implements custom.Status. +func (st *ManagementContextStatus) SetProcessingStatus(status custom.ProcessingStatus) { + // Not implemented +} + // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster @@ -45,6 +91,56 @@ type ManagementContext struct { Status ManagementContextStatus `json:"status,omitempty"` } +// DeepCopyResource implements custom.Context. +func (ctx *ManagementContext) DeepCopyResource() custom.Resource { + return ctx.DeepCopy() +} + +// GetSpec implements custom.Context. +func (ctx *ManagementContext) GetSpec() custom.Spec { + return &ctx.Spec +} + +// GetStatus implements custom.Context. +func (ctx *ManagementContext) GetStatus() custom.Status { + return &ctx.Status +} + +// GetAuth implements custom.Context. +func (ctx *ManagementContext) GetAuth() custom.Auth { + return ctx.Spec.Context.Auth +} + +// GetEnvID implements custom.Context. +func (ctx *ManagementContext) GetEnvID() string { + return ctx.Spec.EnvID +} + +// GetOrgID implements custom.Context. +func (ctx *ManagementContext) GetOrgID() string { + return ctx.Spec.OrgID +} + +// GetSecretRef implements custom.Context. +func (ctx *ManagementContext) GetSecretRef() custom.ResourceRef { + return ctx.Spec.SecretRef() +} + +// GetURL implements custom.Context. +func (ctx *ManagementContext) GetURL() string { + return ctx.Spec.BaseUrl +} + +// HasAuthentication implements custom.Context. +func (ctx *ManagementContext) HasAuthentication() bool { + return ctx.Spec.Auth != nil +} + +// HasSecretRef implements custom.Context. +func (ctx *ManagementContext) HasSecretRef() bool { + return ctx.HasAuthentication() && ctx.Spec.Auth.SecretRef != nil +} + func (ctx *ManagementContext) GetNamespacedName() *refs.NamespacedName { return &refs.NamespacedName{Namespace: ctx.Namespace, Name: ctx.Name} } diff --git a/api/v1alpha1/managementcontext_webhook.go b/api/v1alpha1/managementcontext_webhook.go deleted file mode 100644 index 8c0a08a74..000000000 --- a/api/v1alpha1/managementcontext_webhook.go +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 ( - "context" - "fmt" - - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" - wk "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/webhook" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" - corev1 "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var _ webhook.Defaulter = &ManagementContext{} -var _ webhook.Validator = &ManagementContext{} - -func (ctx *ManagementContext) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(ctx). - Complete() -} - -func (ctx *ManagementContext) Default() {} - -func (ctx *ManagementContext) ValidateCreate() (admission.Warnings, error) { - return validateManagementContext(ctx) -} - -func (ctx *ManagementContext) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { - return validateManagementContext(ctx) -} - -func (*ManagementContext) ValidateDelete() (admission.Warnings, error) { - return admission.Warnings{}, nil -} - -func validateManagementContext(ctx *ManagementContext) (admission.Warnings, error) { - // Make sure the secret exist - if ctx.Spec.HasSecretRef() { - secret := new(corev1.Secret) - err := k8s.GetClient().Get(context.Background(), ctx.Spec.SecretRef().NamespacedName(), secret) - if err != nil { - return admission.Warnings{}, fmt.Errorf("can't create management context [%s] because it is using "+ - "sercret [%v] that doesn't exist in the cluster", ctx.Name, ctx.Spec.SecretRef()) - } - } - - ctxRef := refs.NewNamespacedName(ctx.Namespace, ctx.Name) - if err := wk.CheckAPIMAvailability(&ctxRef); err != nil { - return admission.Warnings{err.Error()}, nil //nolint:nilerr // changed to warning - } - - return admission.Warnings{}, nil -} diff --git a/controllers/apim/apidefinition/controller.go b/controllers/apim/apidefinition/controller.go index cf9a92191..32114edc1 100644 --- a/controllers/apim/apidefinition/controller.go +++ b/controllers/apim/apidefinition/controller.go @@ -41,7 +41,7 @@ const requeueAfterTime = time.Second * 5 func Reconcile( ctx context.Context, - apiDefinition custom.ApiDefinition, + apiDefinition custom.ApiDefinitionResource, r record.EventRecorder, ) (ctrl.Result, error) { logger := log.FromContext(ctx) @@ -66,7 +66,7 @@ func Reconcile( func reconcileApiDefinition( ctx context.Context, - apiDefinition custom.ApiDefinition, + apiDefinition custom.ApiDefinitionResource, events *event.Recorder, ) (ctrl.Result, error) { logger := log.FromContext(ctx) diff --git a/controllers/apim/apidefinition/internal/api_definition_template.go b/controllers/apim/apidefinition/internal/api_definition_template.go index 589d1f42f..9ac4789d8 100644 --- a/controllers/apim/apidefinition/internal/api_definition_template.go +++ b/controllers/apim/apidefinition/internal/api_definition_template.go @@ -33,7 +33,7 @@ import ( // First return value defines if we should requeue or not. func SyncApiDefinitionTemplate( ctx context.Context, - api custom.ApiDefinition, ns string) error { + api custom.ApiDefinitionResource, ns string) error { // We are first looking if the template is in deletion phase, the Kubernetes API marks the object for // deletion by populating .metadata.deletionTimestamp if !api.GetDeletionTimestamp().IsZero() { diff --git a/controllers/apim/apidefinition/internal/config_map.go b/controllers/apim/apidefinition/internal/config_map.go index ddc9c74b8..7a6eb1aab 100644 --- a/controllers/apim/apidefinition/internal/config_map.go +++ b/controllers/apim/apidefinition/internal/config_map.go @@ -38,8 +38,8 @@ const ( gioTypeKey = "gio-type" orgKey = "organizationId" envKey = "environmentId" - defaultEnvId = "DEFAULT" - defaultOrgId = "DEFAULT" + defaultEnvID = "DEFAULT" + defaultOrgID = "DEFAULT" ) func updateConfigMap( @@ -63,7 +63,7 @@ func updateConfigMap( func saveConfigMap( ctx context.Context, - apiDefinition custom.ApiDefinition, + apiDefinition custom.ApiDefinitionResource, ) error { // Create config map with some specific metadata that will be used to check changes across 'Update' events. cm := &v1.ConfigMap{} @@ -93,12 +93,12 @@ func saveConfigMap( definitionVersionKey: apiDefinition.GetResourceVersion(), } - if apiDefinition.OrgID() != "" { - cm.Data[orgKey] = apiDefinition.OrgID() - cm.Data[envKey] = apiDefinition.EnvID() + if apiDefinition.GetOrgID() != "" { + cm.Data[orgKey] = apiDefinition.GetOrgID() + cm.Data[envKey] = apiDefinition.GetEnvID() } else { - cm.Data[orgKey] = defaultOrgId - cm.Data[envKey] = defaultEnvId + cm.Data[orgKey] = defaultOrgID + cm.Data[envKey] = defaultEnvID } var payload any diff --git a/controllers/apim/apidefinition/internal/delete.go b/controllers/apim/apidefinition/internal/delete.go index 7dade1124..bb2c78248 100644 --- a/controllers/apim/apidefinition/internal/delete.go +++ b/controllers/apim/apidefinition/internal/delete.go @@ -25,7 +25,7 @@ import ( util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -func Delete(ctx context.Context, api custom.ApiDefinition) error { +func Delete(ctx context.Context, api custom.ApiDefinitionResource) error { if !util.ContainsFinalizer(api, keys.ApiDefinitionFinalizer) { return nil } @@ -41,17 +41,17 @@ func Delete(ctx context.Context, api custom.ApiDefinition) error { return nil } -func deleteWithContext(ctx context.Context, api custom.ApiDefinition) error { - apim, err := apim.FromContextRef(ctx, api.ContextRef()) +func deleteWithContext(ctx context.Context, api custom.ApiDefinitionResource) error { + apim, err := apim.FromContextRef(ctx, api.ContextRef(), api.GetNamespace()) if err != nil { return err } switch { - case api.Version() == custom.ApiV2: - return errors.IgnoreNotFound(apim.APIs.DeleteV2(api.ID())) - case api.Version() == custom.ApiV4: - return errors.IgnoreNotFound(apim.APIs.DeleteV4(api.ID())) + case api.GetDefinitionVersion() == custom.ApiV2: + return errors.IgnoreNotFound(apim.APIs.DeleteV2(api.GetID())) + case api.GetDefinitionVersion() == custom.ApiV4: + return errors.IgnoreNotFound(apim.APIs.DeleteV4(api.GetID())) default: - return fmt.Errorf("unknown version %s", api.Version()) + return fmt.Errorf("unknown version %s", api.GetDefinitionVersion()) } } diff --git a/controllers/apim/apidefinition/internal/helper.go b/controllers/apim/apidefinition/internal/helper.go index f80ffbb6e..c2af5ef1c 100644 --- a/controllers/apim/apidefinition/internal/helper.go +++ b/controllers/apim/apidefinition/internal/helper.go @@ -17,48 +17,16 @@ package internal import ( "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" apimModel "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/uuid" ) -const separator = "/" - -// For each plan, generate a CrossId from Api Id & Plan Name if not defined. -func generateEmptyPlanCrossIds(spec *v1alpha1.ApiDefinitionV2Spec) { - plans := spec.Plans - - for _, plan := range plans { - if plan.CrossId == "" { - plan.CrossId = uuid.FromStrings(spec.ID, separator, plan.Name) - } - } -} - -func generatePageIDs(api *v1alpha1.ApiDefinition) { - spec := &api.Spec - pages := spec.Pages - for name, page := range pages { - page.API = spec.ID - apiName := api.GetNamespacedName().String() - if page.CrossID == "" { - page.CrossID = uuid.FromStrings(apiName, separator, name) - } - if page.ID == "" { - page.ID = uuid.FromStrings(spec.ID, separator, name) - } - if page.Parent != "" { - page.ParentID = uuid.FromStrings(spec.ID, separator, page.Parent) - } - } -} - // Retrieve the plan ids from the management apiEntity. -func retrieveMgmtPlanIds(spec *v1alpha1.ApiDefinitionV2Spec, mgmtApi *apimModel.ApiEntity) { +func retrieveMgmtPlanIDs(spec *v1alpha1.ApiDefinitionV2Spec, mgmtApi *apimModel.ApiEntity) { plans := spec.Plans for _, plan := range plans { for _, mgmtPlan := range mgmtApi.Plans { - if plan.CrossId == mgmtPlan.CrossId { - plan.Id = mgmtPlan.Id + if plan.CrossID == mgmtPlan.CrossID { + plan.ID = mgmtPlan.ID plan.Api = mgmtPlan.Api } } diff --git a/controllers/apim/apidefinition/internal/update.go b/controllers/apim/apidefinition/internal/update.go index d1fd616af..d271e7703 100644 --- a/controllers/apim/apidefinition/internal/update.go +++ b/controllers/apim/apidefinition/internal/update.go @@ -49,14 +49,14 @@ func createOrUpdateV2(ctx context.Context, apiDefinition *v1alpha1.ApiDefinition spec := &cp.Spec formerStatus := cp.Status - spec.ID = cp.PickID() spec.SetDefinitionContext() - generateEmptyPlanCrossIds(spec) if err := resolveResources(ctx, spec.Resources); err != nil { return err } + cp.PopulateIDs(nil) + if !apiDefinition.HasContext() { if !spec.IsLocal { return errors.NewUnrecoverableError("a context is required when setting local to false") @@ -71,14 +71,11 @@ func createOrUpdateV2(ctx context.Context, apiDefinition *v1alpha1.ApiDefinition log.FromContext(ctx).Info("Syncing API with APIM") - apim, apimErr := apim.FromContextRef(ctx, spec.Context) + apim, apimErr := apim.FromContextRef(ctx, spec.Context, apiDefinition.GetNamespace()) if apimErr != nil { return apimErr } - generatePageIDs(cp) - spec.CrossID = cp.PickCrossID() - _, findErr := apim.APIs.GetByCrossID(spec.CrossID) if errors.IgnoreNotFound(findErr) != nil { return errors.NewContextError(findErr) @@ -100,10 +97,10 @@ func createOrUpdateV2(ctx context.Context, apiDefinition *v1alpha1.ApiDefinition apiDefinition.Status.EnvID = apim.EnvID() apiDefinition.Status.OrgID = apim.OrgID() apiDefinition.Status.State = base.ApiState(mgmtApi.State) - retrieveMgmtPlanIds(spec, mgmtApi) + retrieveMgmtPlanIDs(spec, mgmtApi) if mgmtApi.ShouldSetKubernetesContext() { - if err := apim.APIs.SetKubernetesContext(apiDefinition.ID()); err != nil { + if err := apim.APIs.SetKubernetesContext(apiDefinition.GetID()); err != nil { return errors.NewContextError(err) } } @@ -115,11 +112,11 @@ func createOrUpdateV2(ctx context.Context, apiDefinition *v1alpha1.ApiDefinition if err := deleteConfigMap(ctx, apiDefinition); err != nil { return err } - if err := apim.APIs.Deploy(apiDefinition.ID()); err != nil { + if err := apim.APIs.Deploy(apiDefinition.GetID()); err != nil { return err } if formerStatus.State != spec.State { - return apim.APIs.UpdateState(apiDefinition.ID(), model.ApiStateToAction(spec.State)) + return apim.APIs.UpdateState(apiDefinition.GetID(), model.ApiStateToAction(spec.State)) } return nil @@ -135,18 +132,15 @@ func createOrUpdateV4(ctx context.Context, apiDefinition *v1alpha1.ApiV4Definiti return err } - spec.CrossID = cp.PickCrossID() - spec.Plans = cp.PickPlanIDs() - spec.Pages = cp.PickPageIDs() spec.DefinitionContext = v4.NewDefaultKubernetesContext().MergeWith(spec.DefinitionContext) if spec.Context != nil { log.FromContext(ctx).Info("Syncing API with APIM") - apim, err := apim.FromContextRef(ctx, spec.Context) + apim, err := apim.FromContextRef(ctx, spec.Context, apiDefinition.GetNamespace()) if err != nil { return err } - spec.ID = cp.PickID(apim.Context) + cp.PopulateIDs(apim.Context) status, err := apim.APIs.ImportV4(&spec.Api) if err != nil { return err @@ -154,7 +148,7 @@ func createOrUpdateV4(ctx context.Context, apiDefinition *v1alpha1.ApiV4Definiti apiDefinition.Status.Status = *status log.FromContext(ctx).WithValues("id", spec.ID).Info("API successfully synced with APIM") } else { - spec.ID = cp.PickID(nil) + cp.PopulateIDs(nil) } if spec.DefinitionContext.SyncFrom == v4.OriginManagement || spec.State == base.StateStopped { diff --git a/controllers/apim/application/internal/delete.go b/controllers/apim/application/internal/delete.go index 175947bb8..1c563d96f 100644 --- a/controllers/apim/application/internal/delete.go +++ b/controllers/apim/application/internal/delete.go @@ -32,7 +32,7 @@ func Delete( return nil } - apim, apimErr := apim.FromContextRef(ctx, application.Spec.Context) + apim, apimErr := apim.FromContextRef(ctx, application.Spec.Context, application.GetNamespace()) if apimErr != nil { return apimErr } diff --git a/controllers/apim/application/internal/update.go b/controllers/apim/application/internal/update.go index 05dcf4ae6..50ac1830b 100644 --- a/controllers/apim/application/internal/update.go +++ b/controllers/apim/application/internal/update.go @@ -27,7 +27,7 @@ func CreateOrUpdate(ctx context.Context, application *v1alpha1.Application) erro spec.Origin = "KUBERNETES" spec.ID = application.Status.ID - apim, err := apim.FromContextRef(ctx, spec.Context) + apim, err := apim.FromContextRef(ctx, spec.Context, application.GetNamespace()) if err != nil { return err } diff --git a/docs/api/reference.md b/docs/api/reference.md index fff46daf7..51d9edf1f 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -7091,6 +7091,14 @@ ApiV4DefinitionStatus defines the observed state of API Definition. The environment ID, if a management context has been defined to sync with an APIM instance
false + + errors + object + + When API has been created regardless of errors, this field is +used to persist the error message encountered during admission
+ + false id string @@ -7142,6 +7150,45 @@ to sync the API with an APIM instance
+ +### ApiV4Definition.status.errors +[Go to parent definition](#apiv4definitionstatus) + + + +When API 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
+ ## ApiResource [gravitee.io/v1alpha1](#graviteeiov1alpha1) @@ -7322,7 +7369,7 @@ Application is the main resource handled by the Kubernetes Operator clientId string - The ClientId identifying the application. This field is required when subscribing to an OAUTH2 / JWT plan.
+ The ClientID identifying the application. This field is required when subscribing to an OAUTH2 / JWT plan.
false @@ -7504,7 +7551,7 @@ Application settings client_id string - ClientId is the client id of the application
+ ClientID is the client id of the application
false diff --git a/examples/apim/api_definition/v2/api-with-context.yml b/examples/apim/api_definition/v2/api-with-context.yml index 94ef1acc7..d9f4f21ce 100644 --- a/examples/apim/api_definition/v2/api-with-context.yml +++ b/examples/apim/api_definition/v2/api-with-context.yml @@ -21,7 +21,6 @@ spec: name: "Echo API" contextRef: name: "dev-ctx" - namespace: "default" version: "1.1" description: "Gravitee Kubernetes Operator sample" plans: diff --git a/examples/management_context/debug/management-context-secret.yml b/examples/management_context/debug/management-context-secret.yml new file mode 100644 index 000000000..6c3f4514d --- /dev/null +++ b/examples/management_context/debug/management-context-secret.yml @@ -0,0 +1,22 @@ +# 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: v1 +kind: Secret +metadata: + name: apim-context-credentials +data: + password: YWRtaW4= + username: YWRtaW4= + diff --git a/examples/management_context/debug/management-context-with-secret-ref.yml b/examples/management_context/debug/management-context-with-secret-ref.yml new file mode 100644 index 000000000..d5b49fe9a --- /dev/null +++ b/examples/management_context/debug/management-context-with-secret-ref.yml @@ -0,0 +1,25 @@ +# 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: ManagementContext +metadata: + name: dev-ctx +spec: + baseUrl: http://localhost:8083 + environmentId: DEFAULT + organizationId: DEFAULT + auth: + secretRef: + name: apim-context-credentials + diff --git a/helm/gko/crds/gravitee.io_apiv4definitions.yaml b/helm/gko/crds/gravitee.io_apiv4definitions.yaml index 72f6c597c..363d9ece4 100644 --- a/helm/gko/crds/gravitee.io_apiv4definitions.yaml +++ b/helm/gko/crds/gravitee.io_apiv4definitions.yaml @@ -1278,6 +1278,28 @@ spec: description: The environment ID, if a management context has been defined to sync with an APIM instance type: string + errors: + description: |- + When API 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 API definition in the Gravitee API Management instance (if an API context has been configured). diff --git a/helm/gko/crds/gravitee.io_applications.yaml b/helm/gko/crds/gravitee.io_applications.yaml index 34b450c8d..356e18524 100644 --- a/helm/gko/crds/gravitee.io_applications.yaml +++ b/helm/gko/crds/gravitee.io_applications.yaml @@ -75,7 +75,7 @@ spec: when displaying it on the portal type: string clientId: - description: The ClientId identifying the application. This field + description: The ClientID identifying the application. This field is required when subscribing to an OAUTH2 / JWT plan. type: string contextRef: @@ -164,7 +164,7 @@ spec: app: properties: client_id: - description: ClientId is the client id of the application + description: ClientID is the client id of the application type: string type: description: Application Type diff --git a/internal/admission/webhook/managementcontext.go b/internal/admission/api/base/validate.go similarity index 53% rename from internal/admission/webhook/managementcontext.go rename to internal/admission/api/base/validate.go index 104e01670..a05c92569 100644 --- a/internal/admission/webhook/managementcontext.go +++ b/internal/admission/api/base/validate.go @@ -12,31 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package webhook +package base import ( "context" - "errors" - "fmt" - "net" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/uuid" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/ctxref" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "k8s.io/apimachinery/pkg/runtime" ) -func CheckAPIMAvailability(ctxRef *refs.NamespacedName) error { - cli, err := apim.FromContextRef(context.Background(), ctxRef) - if err != nil { - return fmt.Errorf("can't create apim client for this management context [%s]", ctxRef.Name) - } +func ValidateCreate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() - _, err = cli.APIs.GetV4ByID(uuid.NewV4String()) + errs.Add(ctxref.Validate(ctx, obj)) - var opError *net.OpError - if errors.As(err, &opError) { - return fmt.Errorf("unable to reach APIM, [%s] is not available", cli.Context.BaseUrl) - } + return errs +} - return nil +func ValidateUpdate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + return ValidateCreate(ctx, obj) } diff --git a/internal/admission/api/v2/ctrl.go b/internal/admission/api/v2/ctrl.go new file mode 100644 index 000000000..d8e1b516a --- /dev/null +++ b/internal/admission/api/v2/ctrl.go @@ -0,0 +1,69 @@ +// 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" + + "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{} + +type AdmissionCtrl struct{} + +func (a AdmissionCtrl) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha1.ApiDefinition{}). + WithValidator(a). + WithDefaulter(a). + Complete() +} + +// 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() +} + +// ValidateUpdate implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateUpdate( + ctx context.Context, + oldObj runtime.Object, + newObj runtime.Object, +) (admission.Warnings, error) { + return a.ValidateCreate(ctx, newObj) +} + +// ValidateDelete implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateDelete( + ctx context.Context, + obj runtime.Object, +) (admission.Warnings, error) { + return admission.Warnings{}, nil +} diff --git a/internal/admission/api/v2/validate.go b/internal/admission/api/v2/validate.go new file mode 100644 index 000000000..baa8a291a --- /dev/null +++ b/internal/admission/api/v2/validate.go @@ -0,0 +1,27 @@ +// 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" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "k8s.io/apimachinery/pkg/runtime" +) + +func validateCreate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + return base.ValidateCreate(ctx, obj) +} diff --git a/internal/admission/api/v4/ctrl.go b/internal/admission/api/v4/ctrl.go new file mode 100644 index 000000000..e3ac23d42 --- /dev/null +++ b/internal/admission/api/v4/ctrl.go @@ -0,0 +1,69 @@ +// 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" + + "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{} + +type AdmissionCtrl struct{} + +func (a AdmissionCtrl) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha1.ApiV4Definition{}). + WithValidator(a). + WithDefaulter(a). + Complete() +} + +// 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 admission.Warnings{}, nil +} + +// ValidateUpdate implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateUpdate( + ctx context.Context, + oldObj runtime.Object, + newObj runtime.Object, +) (admission.Warnings, error) { + return a.ValidateCreate(ctx, newObj) +} diff --git a/internal/admission/api/v4/validate.go b/internal/admission/api/v4/validate.go new file mode 100644 index 000000000..3a0a831da --- /dev/null +++ b/internal/admission/api/v4/validate.go @@ -0,0 +1,159 @@ +// 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" + "net/url" + "slices" + + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/v4" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/base" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/env" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s/dynamic" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func validateCreate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + if api, ok := obj.(custom.ApiDefinitionResource); ok { + errs = errs.MergeWith(base.ValidateCreate(ctx, obj)) + if errs.IsSevere() { + return errs + } + errs.Add(validateNoConflictingPath(ctx, api)) + if errs.IsSevere() { + return errs + } + if api.HasContext() { + errs = errs.MergeWith(validateDryRun(ctx, api)) + } + } + return errs +} + +// TODO this should be move to base once implemented for v2 +func validateNoConflictingPath(ctx context.Context, api custom.ApiDefinitionResource) *errors.AdmissionError { + apiPaths, err := api.GetContextPaths() + if err != nil { + return errors.NewSevere(err.Error()) + } + existingPaths, err := getExistingPaths(ctx, api) + if err != nil { + return errors.NewSevere(err.Error()) + } + for _, apiPath := range apiPaths { + if _, pErr := url.Parse(apiPath); pErr != nil { + return errors.NewSevere( + "path [%s] is invalid", + apiPath, + ) + } + if slices.Contains(existingPaths, apiPath) { + return errors.NewSevere( + "invalid API context path [%s]. Another API with the same path already exists", + apiPath, + ) + } + } + return nil +} + +func validateDryRun(ctx context.Context, api custom.ApiDefinitionResource) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + + cp, _ := api.DeepCopyResource().(custom.ApiDefinitionResource) + + apim, err := apim.FromContextRef(ctx, cp.ContextRef(), cp.GetNamespace()) + if err != nil { + errs.AddSevere(err.Error()) + } + + cp.PopulateIDs(apim.Context) + + impl, ok := cp.GetDefinition().(*v4.Api) + if !ok { + errs.AddSevere("unable to call dry run import because api is not a v4 API") + } + + status, err := apim.APIs.DryRunImportV4(impl) + 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 getExistingPaths(ctx context.Context, api custom.ApiDefinitionResource) ([]string, error) { + existingPaths := make([]string, 0) + unstructuredList, err := getListOfExistingApis(ctx, api.GetNamespace()) + if err != nil { + return existingPaths, err + } + + for _, item := range unstructuredList.Items { + converted, cErr := dynamic.Convert(item.Object["spec"], new(v4.Api)) + if cErr != nil { + return existingPaths, cErr + } + convertedPaths, pErr := converted.GetContextPaths() + if pErr != nil { + return existingPaths, pErr + } + if !isCurrentApi(item, api) { + existingPaths = append(existingPaths, convertedPaths...) + } + } + return existingPaths, nil +} + +func isCurrentApi(item unstructured.Unstructured, api custom.ApiDefinitionResource) bool { + return api.GetName() == item.Object["metadata"].(map[string]interface{})["name"] && + api.GetNamespace() == item.Object["metadata"].(map[string]interface{})["namespace"] +} + +func getListOfExistingApis(ctx context.Context, ns string) (*unstructured.UnstructuredList, error) { + gvr := schema.GroupVersionResource{ + Group: "gravitee.io", + Version: "v1alpha1", + Resource: "apiv4definitions", + } + if !env.Config.CheckApiContextPathConflictInCluster { + return dynamic.GetClient(). + Resource(gvr). + Namespace(ns). + List(ctx, metav1.ListOptions{}) + } else { + return dynamic.GetClient(). + Resource(gvr). + List(ctx, metav1.ListOptions{}) + } +} diff --git a/internal/admission/application/ctrl.go b/internal/admission/application/ctrl.go new file mode 100644 index 000000000..cb2b5d988 --- /dev/null +++ b/internal/admission/application/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 application + +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.ManagementContext{}). + 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 admission.Warnings{}, nil +} + +// ValidateUpdate implements admission.CustomValidator. +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/application/validate.go b/internal/admission/application/validate.go new file mode 100644 index 000000000..149bcd659 --- /dev/null +++ b/internal/admission/application/validate.go @@ -0,0 +1,32 @@ +// 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 application + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/ctxref" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" + "k8s.io/apimachinery/pkg/runtime" +) + +func validateCreate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + if app, ok := obj.(custom.ApplicationResource); ok { + errs.Add(ctxref.Validate(ctx, app)) + } + return errs +} diff --git a/internal/admission/ctxref/validate.go b/internal/admission/ctxref/validate.go new file mode 100644 index 000000000..6ed5e05db --- /dev/null +++ b/internal/admission/ctxref/validate.go @@ -0,0 +1,45 @@ +// 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 ctxref + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s/dynamic" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" + "k8s.io/apimachinery/pkg/runtime" +) + +func Validate(ctx context.Context, obj runtime.Object) *errors.AdmissionError { + if ctxAware, ok := obj.(custom.ContextAwareResource); ok { + if ctxAware.HasContext() { + return validateContextRefExists(ctx, ctxAware) + } + } + return nil +} + +func validateContextRefExists(ctx context.Context, ctxAware custom.ContextAwareResource) *errors.AdmissionError { + ctxRef := ctxAware.ContextRef() + if err := dynamic.ExpectResolvedContext(ctx, ctxRef, ctxAware.GetNamespace()); err != nil { + return errors.NewSevere( + "resource [%s] references management context [%v] that doesn't exist in the cluster", + ctxAware.GetName(), + ctxRef, + ) + } + return nil +} diff --git a/internal/admission/mctx/ctrl.go b/internal/admission/mctx/ctrl.go new file mode 100644 index 000000000..4deed1f5a --- /dev/null +++ b/internal/admission/mctx/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 mctx + +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.ManagementContext{}). + 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 admission.Warnings{}, nil +} + +// ValidateUpdate implements admission.CustomValidator. +func (a AdmissionCtrl) ValidateUpdate( + ctx context.Context, + oldObj runtime.Object, + newObj runtime.Object, +) (admission.Warnings, error) { + return admission.Warnings{}, nil +} diff --git a/internal/admission/mctx/validate.go b/internal/admission/mctx/validate.go new file mode 100644 index 000000000..dd0f3c161 --- /dev/null +++ b/internal/admission/mctx/validate.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. + +package mctx + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s/dynamic" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" + "k8s.io/apimachinery/pkg/runtime" +) + +func validateCreate(ctx context.Context, obj runtime.Object) *errors.AdmissionErrors { + errs := errors.NewAdmissionErrors() + + if context, ok := obj.(custom.ContextResource); ok { + errs.Add(validateSecretRef(ctx, context)) + errs.Add(validateContextIsAvailable(ctx, context)) + } + + return errs +} + +func validateSecretRef(ctx context.Context, context custom.ContextResource) *errors.AdmissionError { + if context.HasSecretRef() { + if err := dynamic.ExpectResolvedSecret(ctx, context.GetSecretRef(), context.GetNamespace()); err != nil { + return errors.NewSevere( + "secret [%v] doesn't exist in the cluster", + context.GetSecretRef(), + ) + } + } + return nil +} + +func validateContextIsAvailable(ctx context.Context, context custom.ContextResource) *errors.AdmissionError { + apim, err := apim.FromContext(ctx, context, context.GetNamespace()) + if err != nil { + return errors.NewSevere(err.Error()) + } + _, err = apim.Env.Get() + + if errors.IsNetworkError(err) { + return errors.NewWarning( + "unable to reach APIM, [%s] is not available", + apim.Context.GetURL(), + ) + } + + if errors.IsUnauthorized(err) { + return errors.NewSevere( + "bad credentials for context [%s]", + context.GetName(), + ) + } + + if errors.IsBadRequest(err) { + return errors.NewSevere(err.Error()) + } + + return nil +} diff --git a/internal/admission/webhook/apiv4defenition.go b/internal/admission/webhook/apiv4defenition.go deleted file mode 100644 index 92ccaa434..000000000 --- a/internal/admission/webhook/apiv4defenition.go +++ /dev/null @@ -1,159 +0,0 @@ -// 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 webhook - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "strings" - - v4 "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/v4" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/refs" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/env" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -func ValidateApiV4(ctx context.Context, api *v4.Api, name, ns string, - ctxRef *refs.NamespacedName) (admission.Warnings, error) { - // make sure Management Context exist before creating the API Definition resource - if ctxRef != nil { //nolint:nestif // nested if is needed - gvr := schema.GroupVersionResource{ - Group: "gravitee.io", - Version: "v1alpha1", - Resource: "apiv4definitions", - } - if _, err := k8s.GetDynamicClient(). - Resource(gvr). - Namespace(ctxRef.Namespace). - Get(ctx, ctxRef.Name, metav1.GetOptions{}); err != nil { - return admission.Warnings{}, fmt.Errorf("can't create API [%s] because it is using "+ - "management context [%v] that doesn't exist in the cluster", api.Name, ctxRef) - } - } else { - // check for unique context path - apis, err := getListOfExistingApis(ctx, ns) - - if err != nil { - return admission.Warnings{}, fmt.Errorf("can't list existing APIs") - } - - existingListeners := make([]*v4.GenericListener, 0) - for _, item := range apis.Items { - bytes, mErr := json.Marshal(item.Object["spec"]) - if mErr != nil { - return admission.Warnings{}, mErr - } - - ea := new(v4.Api) - err = json.Unmarshal(bytes, ea) - if err != nil { - return admission.Warnings{}, err - } - - if name != item.Object["metadata"].(map[string]interface{})["name"] || - ns != item.Object["metadata"].(map[string]interface{})["namespace"] { - existingListeners = append(existingListeners, ea.Listeners...) - } - } - - if err = validateApiContextPath(existingListeners, api.Listeners); err != nil { - return admission.Warnings{}, err - } - } - - return admission.Warnings{}, nil -} - -func getListOfExistingApis(ctx context.Context, ns string) (*unstructured.UnstructuredList, error) { - gvr := schema.GroupVersionResource{ - Group: "gravitee.io", - Version: "v1alpha1", - Resource: "apiv4definitions", - } - if !env.Config.CheckApiContextPathConflictInCluster { - return k8s.GetDynamicClient(). - Resource(gvr). - Namespace(ns). - List(ctx, metav1.ListOptions{}) - } else { - return k8s.GetDynamicClient(). - Resource(gvr). - List(ctx, metav1.ListOptions{}) - } -} - -func validateApiContextPath(existingListeners, listeners []*v4.GenericListener) error { - apiPaths := make([]string, 0) - for _, l := range listeners { - for _, s := range parseListener(l) { - p, err := url.Parse(s) - if err != nil { - return err - } - apiPaths = append(apiPaths, p.String()) - } - } - - for _, l := range existingListeners { - paths := parseListener(l) - err := findDuplicatePath(paths, apiPaths) - if err != nil { - return err - } - } - - return nil -} - -func parseListener(l v4.Listener) []string { - if l == nil { - return []string{} - } - - switch t := l.(type) { - case *v4.GenericListener: - return parseListener(t.ToListener()) - case *v4.HttpListener: - { - paths := make([]string, 0) - for _, path := range t.Paths { - p := fmt.Sprintf("%s/%s", path.Host, path.Path) - paths = append(paths, strings.ReplaceAll(p, "//", "/")) - } - return paths - } - case *v4.TCPListener: - return t.Hosts - } - - return []string{} -} - -func findDuplicatePath(existingPaths []string, newPaths []string) error { - for _, ep := range existingPaths { - for _, np := range newPaths { - if ep == np { - return fmt.Errorf("invalid API context path [%s]. Another API with the same path already exists", ep) - } - } - } - return nil -} diff --git a/internal/apim/apim.go b/internal/apim/apim.go index feb0d756e..3a811509b 100644 --- a/internal/apim/apim.go +++ b/internal/apim/apim.go @@ -17,116 +17,57 @@ package apim import ( "context" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/client" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/service" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/http" - "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s/dynamic" "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" - - coreV1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -const ( - bearerTokenSecretKey = "bearerToken" - usernameSecretKey = "username" - passwordSecretKey = "password" ) // 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 + Env *service.Env - Context *management.Context + Context custom.Context } // EnvID returns the environment ID of the current managed APIM instance. func (apim *APIM) EnvID() string { - return apim.Context.EnvId + return apim.Context.GetEnvID() } // OrgID returns the organization ID of the current managed APIM instance. func (apim *APIM) OrgID() string { - return apim.Context.OrgId + return apim.Context.GetOrgID() } // FromContext returns a new APIM instance from a given reconcile context and management context. -func FromContext(ctx context.Context, managementContext *management.Context) (*APIM, error) { - orgID, envID := managementContext.OrgId, managementContext.EnvId - urls, err := client.NewURLs(managementContext.BaseUrl, orgID, envID) +func FromContext(ctx context.Context, context custom.Context, parentNs string) (*APIM, error) { + orgID, envID := context.GetOrgID(), context.GetEnvID() + urls, err := client.NewURLs(context.GetURL(), orgID, envID) if err != nil { return nil, err } client := &client.Client{ - HTTP: http.NewClient(ctx, toHttpAuth(managementContext)), + HTTP: http.NewClient(ctx, toHttpAuth(context)), URLs: urls, } return &APIM{ APIs: service.NewAPIs(client), Applications: service.NewApplications(client), - Context: managementContext, + Env: service.NewEnv(client), + Context: context, }, nil } -func FromContextRef(ctx context.Context, ref custom.ResourceRef) (*APIM, error) { - managementContext, err := resolveContext(ctx, ref) - if err != nil { - return nil, err - } - return FromContext(ctx, managementContext) -} - -func resolveContext( - ctx context.Context, - ref custom.ResourceRef, -) (*management.Context, error) { - log.FromContext(ctx). - WithValues("namespace", ref.GetNamespace()). - WithValues("name", ref.GetName()). - Info("Resolving management context") - - context, err := k8s.ResolveContext(ctx, ref) +func FromContextRef(ctx context.Context, ref custom.ResourceRef, parentNs string) (*APIM, error) { + context, err := dynamic.ResolveContext(ctx, ref, parentNs) if err != nil { return nil, err } - - if err = resolveContextSecrets(ctx, context, ref); err != nil { - return nil, err - } - - return context, nil -} - -func resolveContextSecrets(ctx context.Context, context *management.Context, ref custom.ResourceRef) error { - if context.HasSecretRef() { - secret := new(coreV1.Secret) - - secretKey := context.SecretRef().NamespacedName() - secretKey.Namespace = getSecretNamespace(context, ref) - - if err := k8s.GetClient().Get(ctx, secretKey, secret); err != nil { - return err - } - - bearerToken := string(secret.Data[bearerTokenSecretKey]) - username := string(secret.Data[usernameSecretKey]) - password := string(secret.Data[passwordSecretKey]) - - context.SetToken(bearerToken) - context.SetCredentials(username, password) - } - - return nil -} - -func getSecretNamespace(context *management.Context, ref custom.ResourceRef) string { - secretRef := context.SecretRef() - if secretRef.Namespace != "" { - return secretRef.Namespace - } - return ref.GetNamespace() + return FromContext(ctx, context, parentNs) } diff --git a/internal/apim/auth.go b/internal/apim/auth.go index 5a4a7f044..025f95815 100644 --- a/internal/apim/auth.go +++ b/internal/apim/auth.go @@ -15,36 +15,36 @@ package apim import ( - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/http" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" ) -func toHttpAuth(management *management.Context) *http.Auth { +func toHttpAuth(management custom.Context) *http.Auth { if !management.HasAuthentication() { return nil } return &http.Auth{ - Basic: toBasicAuth(management.Auth), - Token: toBearer(management.Auth), + Basic: toBasicAuth(management.GetAuth()), + Token: toBearer(management.GetAuth()), } } -func toBasicAuth(auth *management.Auth) *http.BasicAuth { - if auth == nil || auth.Credentials == nil { +func toBasicAuth(auth custom.Auth) *http.BasicAuth { + if auth == nil || !auth.HasCredentials() { return nil } return &http.BasicAuth{ - Username: auth.Credentials.Username, - Password: auth.Credentials.Password, + Username: auth.GetCredentials().GetUsername(), + Password: auth.GetCredentials().GetPassword(), } } -func toBearer(auth *management.Auth) http.BearerToken { +func toBearer(auth custom.Auth) http.BearerToken { if auth == nil { return "" } - return http.BearerToken(auth.BearerToken) + return http.BearerToken(auth.GetBearerToken()) } diff --git a/internal/apim/model/api.go b/internal/apim/model/api.go index e06497929..cb2a4409f 100644 --- a/internal/apim/model/api.go +++ b/internal/apim/model/api.go @@ -54,7 +54,7 @@ func NewKubernetesContext() *DefinitionContext { } type ApiListItem struct { - Id string `json:"id"` + ID string `json:"id"` Name string `json:"name"` State string `json:"state"` Visibility string `json:"visibility"` @@ -80,8 +80,8 @@ func ApiStateToAction(s base.ApiState) Action { } type Plan struct { - Id string `json:"id"` - CrossId string `json:"crossId"` + ID string `json:"id"` + CrossID string `json:"crossId"` Name string `json:"name"` Security PlanSecurityType `json:"security"` Status PlanStatus `json:"status"` diff --git a/internal/apim/model/api_key.go b/internal/apim/model/api_key.go index d6649ba92..b424bd349 100644 --- a/internal/apim/model/api_key.go +++ b/internal/apim/model/api_key.go @@ -15,6 +15,6 @@ package model type ApiKeyEntity struct { - Id string `json:"id"` + ID string `json:"id"` Key string `json:"key"` } diff --git a/internal/apim/model/application.go b/internal/apim/model/application.go index 585bc30a4..9668d56b1 100644 --- a/internal/apim/model/application.go +++ b/internal/apim/model/application.go @@ -19,7 +19,7 @@ import ( ) type Application struct { - Id string `json:"id,omitempty"` + ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Status string `json:"status,omitempty"` Description string `json:"description,omitempty"` @@ -34,7 +34,7 @@ type Application struct { type ApplicationMetaData struct { Name string `json:"name"` - ApplicationId string `json:"applicationId,omitempty"` + ApplicationID string `json:"applicationId,omitempty"` Value string `json:"value,omitempty"` DefaultValue string `json:"defaultValue,omitempty"` Format *application.MetaDataFormat `json:"format,omitempty"` diff --git a/pkg/types/list/list.go b/internal/apim/model/env.go similarity index 76% rename from pkg/types/list/list.go rename to internal/apim/model/env.go index c6a087eeb..77f600319 100644 --- a/pkg/types/list/list.go +++ b/internal/apim/model/env.go @@ -12,16 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package list +package model -import "sigs.k8s.io/controller-runtime/pkg/client" - -type Item interface { - GetName() string - GetNamespace() string -} - -type Interface interface { - client.ObjectList - GetItems() []Item +type Env struct { + ID string `json:"id"` + Name string `json:"name"` } diff --git a/internal/apim/model/subscription.go b/internal/apim/model/subscription.go index e9b35b5c4..ed8ca5919 100644 --- a/internal/apim/model/subscription.go +++ b/internal/apim/model/subscription.go @@ -15,5 +15,5 @@ package model type Subscription struct { - Id string `json:"id"` + ID string `json:"id"` } diff --git a/internal/apim/service/apis.go b/internal/apim/service/apis.go index 8ed5a660d..a828a7741 100644 --- a/internal/apim/service/apis.go +++ b/internal/apim/service/apis.go @@ -16,6 +16,7 @@ package service import ( "net/http" + "strconv" v4 "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/v4" @@ -103,7 +104,15 @@ func (svc *APIs) ImportV2(method string, spec *v2.Api) (*model.ApiEntity, error) } func (svc *APIs) ImportV4(spec *v4.Api) (*v4.Status, error) { - url := svc.EnvV2Target("apis/_import/crd") + return svc.importV4(spec, false) +} + +func (svc *APIs) DryRunImportV4(spec *v4.Api) (*v4.Status, error) { + return svc.importV4(spec, true) +} + +func (svc *APIs) importV4(spec *v4.Api, dryRun bool) (*v4.Status, error) { + url := svc.EnvV2Target("apis/_import/crd").WithQueryParam("dryRun", strconv.FormatBool(dryRun)) status := new(v4.Status) if err := svc.HTTP.Put(url.String(), spec, status); err != nil { diff --git a/internal/apim/service/applications.go b/internal/apim/service/applications.go index 31ed669b4..9949b439d 100644 --- a/internal/apim/service/applications.go +++ b/internal/apim/service/applications.go @@ -51,12 +51,12 @@ func (svc *Applications) Search(query string, status string) ([]model.Applicatio return *applications, nil } -func (svc *Applications) GetByID(appId string) (*model.Application, error) { - if appId == "" { +func (svc *Applications) GetByID(appID string) (*model.Application, error) { + if appID == "" { return nil, errors.NewNotFoundError() } - url := svc.EnvV1Target(applicationsPath).WithPath(appId) + url := svc.EnvV1Target(applicationsPath).WithPath(appID) application := new(model.Application) if err := svc.HTTP.Get(url.String(), application); err != nil { @@ -66,12 +66,12 @@ func (svc *Applications) GetByID(appId string) (*model.Application, error) { return application, nil } -func (svc *Applications) GetMetadataByApplicationID(appId string) (*[]model.ApplicationMetaData, error) { - if appId == "" { +func (svc *Applications) GetMetadataByApplicationID(appID string) (*[]model.ApplicationMetaData, error) { + if appID == "" { return nil, fmt.Errorf("can't retrieve metadata without application id") } - url := svc.EnvV1Target(applicationsPath).WithPath(appId).WithPath(metadataPath) + url := svc.EnvV1Target(applicationsPath).WithPath(appID).WithPath(metadataPath) application := new([]model.ApplicationMetaData) if err := svc.HTTP.Get(url.String(), application); err != nil { @@ -92,14 +92,14 @@ func (svc *Applications) CreateOrUpdate(spec *application.Application) (*applica return status, nil } -func (svc *Applications) Delete(appId string) error { - url := svc.EnvV1Target(applicationsPath).WithPath(appId) +func (svc *Applications) Delete(appID string) error { + url := svc.EnvV1Target(applicationsPath).WithPath(appID) return svc.HTTP.Delete(url.String(), nil) } -func (svc *Applications) CreateOrUpdateMetadata(method string, appId string, +func (svc *Applications) CreateOrUpdateMetadata(method string, appID string, spec any, key string) (*model.ApplicationMetaData, error) { - url := svc.EnvV1Target(applicationsPath).WithPath(appId).WithPath(metadataPath) + url := svc.EnvV1Target(applicationsPath).WithPath(appID).WithPath(metadataPath) fun := svc.HTTP.Post if method == http.MethodPut { url = url.WithPath(key) @@ -114,7 +114,7 @@ func (svc *Applications) CreateOrUpdateMetadata(method string, appId string, return md, nil } -func (svc *Applications) DeleteMetadata(appId string, key string) error { - url := svc.EnvV1Target(applicationsPath).WithPath(appId).WithPath(metadataPath).WithPath(key) +func (svc *Applications) DeleteMetadata(appID string, key string) error { + url := svc.EnvV1Target(applicationsPath).WithPath(appID).WithPath(metadataPath).WithPath(key) return svc.HTTP.Delete(url.String(), nil) } diff --git a/internal/apim/service/env.go b/internal/apim/service/env.go index 3b99280d2..c066fd500 100644 --- a/internal/apim/service/env.go +++ b/internal/apim/service/env.go @@ -31,3 +31,11 @@ func (svc *Env) CreateGroup(group *model.Group) error { url := svc.EnvV1Target("configuration").WithPath("groups") return svc.HTTP.Post(url.String(), group, group) } + +func (svc *Env) Get() (*model.Env, error) { + env := new(model.Env) + if err := svc.HTTP.Get(svc.URLs.EnvV2.String(), env); err != nil { + return nil, err + } + return env, nil +} diff --git a/internal/errors/admission.go b/internal/errors/admission.go new file mode 100644 index 000000000..61d50d165 --- /dev/null +++ b/internal/errors/admission.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 errors + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +type Severity string + +const ( + Severe = Severity("severe") + Warning = Severity("warning") +) + +type AdmissionError struct { + Severity Severity + Message string +} + +type AdmissionErrors struct { + Warning []*AdmissionError + Severe []*AdmissionError +} + +func NewAdmissionErrors() *AdmissionErrors { + return &AdmissionErrors{ + Severe: make([]*AdmissionError, 0), + Warning: make([]*AdmissionError, 0), + } +} + +func (errs *AdmissionErrors) Map() (admission.Warnings, error) { + warnings := admission.Warnings{} + for _, w := range errs.Warning { + warnings = append(warnings, w.Error()) + } + if len(errs.Severe) == 0 { + return warnings, nil + } + return warnings, errs.Severe[0] +} + +func (errs *AdmissionErrors) MergeWith(other *AdmissionErrors) *AdmissionErrors { + if other == nil { + return errs + } + errs.Warning = append(errs.Warning, other.Warning...) + errs.Severe = append(errs.Severe, other.Severe...) + return errs +} + +func (errs *AdmissionErrors) IsSevere() bool { + return len(errs.Severe) > 0 +} + +func (errs *AdmissionErrors) AddSevere(format string, args ...any) { + errs.Severe = append(errs.Severe, NewSevere(format, args...)) +} + +func (errs *AdmissionErrors) AddWarning(format string, args ...any) { + errs.Warning = append(errs.Warning, NewWarning(format, args...)) +} + +func (errs *AdmissionErrors) Add(err *AdmissionError) { + if err == nil { + return + } + if err.Severity == Severe { + errs.Severe = append(errs.Severe, err) + } + if err.Severity == Warning { + errs.Warning = append(errs.Warning, err) + } +} + +func (err *AdmissionError) Error() string { + return err.Message +} + +func NewSevere(format string, args ...any) *AdmissionError { + return &AdmissionError{ + Severity: Severe, + Message: fmt.Sprintf(format, args...), + } +} + +func NewWarning(format string, args ...any) *AdmissionError { + return &AdmissionError{ + Severity: Warning, + Message: fmt.Sprintf(format, args...), + } +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 418dfc726..d83a2c3be 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" kErrors "k8s.io/apimachinery/pkg/util/errors" @@ -129,6 +130,19 @@ func IsNotFound(err error) bool { return false } +func IsBadRequest(err error) bool { + serverError := &ServerError{} + if errors.As(err, serverError) { + return serverError.StatusCode == http.StatusBadRequest + } + return false +} + +func IsNetworkError(err error) bool { + opErr := new(net.OpError) + return errors.As(err, &opErr) +} + func IsUnauthorized(err error) bool { serverError := &ServerError{} if errors.As(err, serverError) { diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go index 4f7047cad..6b5a5737e 100644 --- a/internal/indexer/indexer.go +++ b/internal/indexer/indexer.go @@ -17,7 +17,7 @@ package indexer import ( "context" - gio "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/keys" v1 "k8s.io/api/networking/v1" @@ -54,29 +54,34 @@ func InitCache(ctx context.Context, cache cache.Cache) error { errs := make([]error, 0) contextIndexer := newIndexer(ApiContextField, indexManagementContexts) - if err := cache.IndexField(ctx, &gio.ApiDefinition{}, contextIndexer.Field, contextIndexer.Func); err != nil { + if err := cache.IndexField(ctx, &v1alpha1.ApiDefinition{}, contextIndexer.Field, contextIndexer.Func); err != nil { errs = append(errs, err) } apiV4ContextIndexer := newIndexer(ApiV4ContextField, indexApiV4ManagementContexts) - if err := cache.IndexField(ctx, &gio.ApiV4Definition{}, apiV4ContextIndexer.Field, + if err := cache.IndexField(ctx, &v1alpha1.ApiV4Definition{}, apiV4ContextIndexer.Field, apiV4ContextIndexer.Func); err != nil { errs = append(errs, err) } resourceIndexer := newIndexer(ApiResourceField, indexApiResourceRefs) - if err := cache.IndexField(ctx, &gio.ApiDefinition{}, resourceIndexer.Field, resourceIndexer.Func); err != nil { + if err := cache.IndexField(ctx, &v1alpha1.ApiDefinition{}, resourceIndexer.Field, resourceIndexer.Func); err != nil { errs = append(errs, err) } apiV4ResourceIndexer := newIndexer(ApiV4ResourceField, indexIApiV4ResourceRefs) - if err := cache.IndexField(ctx, &gio.ApiV4Definition{}, apiV4ResourceIndexer.Field, + if err := cache.IndexField(ctx, &v1alpha1.ApiV4Definition{}, apiV4ResourceIndexer.Field, apiV4ResourceIndexer.Func); err != nil { errs = append(errs, err) } secretRefIndexer := newIndexer(SecretRefField, indexManagementContextSecrets) - if err := cache.IndexField(ctx, &gio.ManagementContext{}, secretRefIndexer.Field, secretRefIndexer.Func); err != nil { + if err := cache.IndexField( + ctx, + &v1alpha1.ManagementContext{}, + secretRefIndexer.Field, + secretRefIndexer.Func, + ); err != nil { errs = append(errs, err) } @@ -91,7 +96,7 @@ func InitCache(ctx context.Context, cache cache.Cache) error { } appContextIndexer := newIndexer(AppContextField, indexApplicationManagementContexts) - if err := cache.IndexField(ctx, &gio.Application{}, appContextIndexer.Field, appContextIndexer.Func); err != nil { + if err := cache.IndexField(ctx, &v1alpha1.Application{}, appContextIndexer.Field, appContextIndexer.Func); err != nil { errs = append(errs, err) } @@ -120,7 +125,7 @@ func newIndexer[T client.Object](field IndexField, doIndex func(T, *[]string)) I } } -func indexManagementContexts(api *gio.ApiDefinition, fields *[]string) { +func indexManagementContexts(api *v1alpha1.ApiDefinition, fields *[]string) { if api.Spec.Context == nil { return } @@ -128,7 +133,7 @@ func indexManagementContexts(api *gio.ApiDefinition, fields *[]string) { *fields = append(*fields, api.Spec.Context.String()) } -func indexApiV4ManagementContexts(api *gio.ApiV4Definition, fields *[]string) { +func indexApiV4ManagementContexts(api *v1alpha1.ApiV4Definition, fields *[]string) { if api.Spec.Context == nil { return } @@ -136,13 +141,13 @@ func indexApiV4ManagementContexts(api *gio.ApiV4Definition, fields *[]string) { *fields = append(*fields, api.Spec.Context.String()) } -func indexManagementContextSecrets(context *gio.ManagementContext, fields *[]string) { +func indexManagementContextSecrets(context *v1alpha1.ManagementContext, fields *[]string) { if context.Spec.HasSecretRef() { *fields = append(*fields, context.Spec.SecretRef().String()) } } -func indexApiResourceRefs(api *gio.ApiDefinition, fields *[]string) { +func indexApiResourceRefs(api *v1alpha1.ApiDefinition, fields *[]string) { if api.Spec.Resources == nil { return } @@ -154,7 +159,7 @@ func indexApiResourceRefs(api *gio.ApiDefinition, fields *[]string) { } } -func indexIApiV4ResourceRefs(api *gio.ApiV4Definition, fields *[]string) { +func indexIApiV4ResourceRefs(api *v1alpha1.ApiV4Definition, fields *[]string) { if api.Spec.Resources == nil { return } @@ -188,7 +193,7 @@ func indexTLSSecret(ing *v1.Ingress, fields *[]string) { } } -func indexApplicationManagementContexts(application *gio.Application, fields *[]string) { +func indexApplicationManagementContexts(application *v1alpha1.Application, fields *[]string) { if application.Spec.Context == nil { return } diff --git a/internal/k8s/client.go b/internal/k8s/client.go index 7130919be..0648a9920 100644 --- a/internal/k8s/client.go +++ b/internal/k8s/client.go @@ -15,29 +15,15 @@ package k8s import ( - "sync" - - "k8s.io/client-go/dynamic" "sigs.k8s.io/controller-runtime/pkg/client" - - ctrl "sigs.k8s.io/controller-runtime" ) var cli client.Client -var dynamicClient *dynamic.DynamicClient -var once sync.Once func RegisterClient(c client.Client) { - once.Do(func() { - cli = c - dynamicClient = dynamic.NewForConfigOrDie(ctrl.GetConfigOrDie()) - }) + cli = c } func GetClient() client.Client { return cli } - -func GetDynamicClient() *dynamic.DynamicClient { - return dynamicClient -} diff --git a/internal/admission/common/mutate/context.go b/internal/k8s/dynamic/client.go similarity index 66% rename from internal/admission/common/mutate/context.go rename to internal/k8s/dynamic/client.go index 5f7312680..dec751eab 100644 --- a/internal/admission/common/mutate/context.go +++ b/internal/k8s/dynamic/client.go @@ -12,12 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mutate +package dynamic -import "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" +import ( + "sync" -func SetDefaults(ctxAware custom.ContextAwareResource) { - if ctxAware.HasContext() && ctxAware.ContextRef().IsMissingNamespace() { - ctxAware.ContextRef().SetNamespace(ctxAware.GetNamespace()) - } + "k8s.io/client-go/dynamic" + + ctrl "sigs.k8s.io/controller-runtime" +) + +var dynamicClient *dynamic.DynamicClient +var once sync.Once + +func GetClient() *dynamic.DynamicClient { + once.Do(func() { + dynamicClient = dynamic.NewForConfigOrDie(ctrl.GetConfigOrDie()) + }) + return dynamicClient } diff --git a/internal/k8s/dynamic/convert.go b/internal/k8s/dynamic/convert.go new file mode 100644 index 000000000..2edbc0f78 --- /dev/null +++ b/internal/k8s/dynamic/convert.go @@ -0,0 +1,47 @@ +// 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 dynamic + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func Convert[T any](source any, target T) (T, error) { + b, err := json.Marshal(source) + if err != nil { + return target, err + } + + if err = json.Unmarshal(b, target); err != nil { + return target, err + } + + return target, nil +} + +func ConvertList[T any](list *unstructured.UnstructuredList) ([]T, error) { + apis := make([]T, 0) + t := new(T) + for _, it := range list.Items { + api, err := Convert(it.Object["spec"], *t) + if err != nil { + return apis, err + } + apis = append(apis, api) + } + return apis, nil +} diff --git a/internal/k8s/dynamic/mctx.go b/internal/k8s/dynamic/mctx.go new file mode 100644 index 000000000..f2a4b0ee8 --- /dev/null +++ b/internal/k8s/dynamic/mctx.go @@ -0,0 +1,66 @@ +// 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 dynamic + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/keys" + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + bearerTokenSecretKey = "bearerToken" + usernameSecretKey = "username" + passwordSecretKey = "password" +) + +func ExpectResolvedContext(ctx context.Context, ref custom.ResourceRef, parentNs string) error { + if _, err := ResolveContext(ctx, ref, parentNs); err != nil { + return err + } + return nil +} + +func ResolveContext(ctx context.Context, ref custom.ResourceRef, parentNs string) (*management.Context, error) { + context, err := resolveRefSpec(ctx, ref, parentNs, schema.GroupVersionResource{ + Group: keys.CrdGroup, + Version: keys.CrdVersion, + Resource: "managementcontexts", + }, new(management.Context)) + if err != nil { + return nil, err + } + + return injectSecretIfAny(ctx, context, parentNs) +} + +func injectSecretIfAny(ctx context.Context, mCtx *management.Context, parentNs string) (*management.Context, error) { + if mCtx.HasSecretRef() { + secret, err := ResolveSecret(ctx, mCtx.SecretRef(), parentNs) + if err != nil { + return nil, err + } + bearerToken := string(secret.Data[bearerTokenSecretKey]) + username := string(secret.Data[usernameSecretKey]) + password := string(secret.Data[passwordSecretKey]) + + mCtx.SetToken(bearerToken) + mCtx.SetCredentials(username, password) + } + return mCtx, nil +} diff --git a/internal/k8s/ref.go b/internal/k8s/dynamic/resolve.go similarity index 55% rename from internal/k8s/ref.go rename to internal/k8s/dynamic/resolve.go index efa8b644b..1c9d8eeec 100644 --- a/internal/k8s/ref.go +++ b/internal/k8s/dynamic/resolve.go @@ -12,61 +12,57 @@ // See the License for the specific language governing permissions and // limitations under the License. -package k8s +package dynamic import ( "context" - "encoding/json" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" - "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/keys" "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" - coreV1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) -func ResolveContext(ctx context.Context, ref custom.ResourceRef) (*management.Context, error) { - return resolveRef(ctx, ref, schema.GroupVersionResource{ - Group: keys.CrdGroup, - Version: keys.CrdVersion, - Resource: "managementcontexts", - }, new(management.Context)) -} - -func ResolveSecret(ctx context.Context, ref custom.ResourceRef) (*coreV1.Secret, error) { - return resolveRef(ctx, ref, schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "secrets", - }, new(coreV1.Secret)) -} - -func resolveRef[T any]( +func resolveRefSpec[T any]( ctx context.Context, ref custom.ResourceRef, + parentNs string, gvr schema.GroupVersionResource, target T, ) (T, error) { - dynamic, err := GetDynamicClient(). - Resource(gvr). - Namespace(ref.GetNamespace()). - Get(ctx, ref.GetName(), v1.GetOptions{}) - + dynamic, err := resolveDynamic(ctx, ref, parentNs, gvr) if err != nil { return target, err } + return Convert(dynamic.Object["spec"], target) +} - spec := dynamic.Object["spec"] - - b, err := json.Marshal(spec) +func resolveRef[T any]( + ctx context.Context, + ref custom.ResourceRef, + parentNs string, + gvr schema.GroupVersionResource, + target T, +) (T, error) { + dynamic, err := resolveDynamic(ctx, ref, parentNs, gvr) if err != nil { return target, err } + return Convert(dynamic.Object, target) +} - if err = json.Unmarshal(b, target); err != nil { - return target, err +func resolveDynamic( + ctx context.Context, + ref custom.ResourceRef, + parentNs string, + gvr schema.GroupVersionResource, +) (*unstructured.Unstructured, error) { + if ref.GetNamespace() == "" { + ref.SetNamespace(parentNs) } - return target, nil + return GetClient(). + Resource(gvr). + Namespace(ref.GetNamespace()). + Get(ctx, ref.GetName(), v1.GetOptions{}) } diff --git a/internal/k8s/dynamic/secret.go b/internal/k8s/dynamic/secret.go new file mode 100644 index 000000000..8d697074f --- /dev/null +++ b/internal/k8s/dynamic/secret.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 dynamic + +import ( + "context" + + coreV1 "k8s.io/api/core/v1" + + "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/types/k8s/custom" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func ExpectResolvedSecret(ctx context.Context, ref custom.ResourceRef, parentNs string) error { + if _, err := ResolveSecret(ctx, ref, parentNs); err != nil { + return err + } + return nil +} + +func ResolveSecret(ctx context.Context, ref custom.ResourceRef, parentNs string) (*coreV1.Secret, error) { + return resolveRef(ctx, ref, parentNs, schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + }, new(coreV1.Secret)) +} diff --git a/internal/template/resolver.go b/internal/template/resolver.go index 90bd6d3a3..72bd7245f 100644 --- a/internal/template/resolver.go +++ b/internal/template/resolver.go @@ -22,13 +22,13 @@ import ( "strings" "text/template" + "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" "github.com/gravitee-io/gravitee-kubernetes-operator/pkg/keys" netv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" "gopkg.in/yaml.v3" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/main.go b/main.go index 771fc42dc..1144fef25 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,10 @@ import ( "os" "strings" + 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" + mctxAdmission "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/k8s" wk "github.com/gravitee-io/gravitee-kubernetes-operator/internal/webhook" "gopkg.in/yaml.v3" @@ -333,16 +337,16 @@ func setupAdmissionWebhooks(mgr manager.Manager) error { if err := (&v1alpha1.ApiResource{}).SetupWebhookWithManager(mgr); err != nil { return err } - if err := (&v1alpha1.ApiDefinition{}).SetupWebhookWithManager(mgr); err != nil { + if err := (v2Admission.AdmissionCtrl{}).SetupWithManager(mgr); err != nil { return err } - if err := (&v1alpha1.ApiV4Definition{}).SetupWebhookWithManager(mgr); err != nil { + if err := (v4Admission.AdmissionCtrl{}).SetupWithManager(mgr); err != nil { return err } - if err := (&v1alpha1.Application{}).SetupWebhookWithManager(mgr); err != nil { + if err := (appAdmission.AdmissionCtrl{}).SetupWithManager(mgr); err != nil { return err } - if err := (&v1alpha1.ManagementContext{}).SetupWebhookWithManager(mgr); err != nil { + if err := (mctxAdmission.AdmissionCtrl{}).SetupWithManager(mgr); err != nil { return err } diff --git a/pkg/types/k8s/custom/custom.go b/pkg/types/k8s/custom/custom.go index 09018096f..1bbc41b87 100644 --- a/pkg/types/k8s/custom/custom.go +++ b/pkg/types/k8s/custom/custom.go @@ -45,10 +45,21 @@ type Resource interface { // +k8s:deepcopy-gen=false type ApiDefinition interface { + GetDefinitionVersion() ApiDefinitionVersion + GetContextPaths() ([]string, error) +} + +// +k8s:deepcopy-gen=false +type ApiDefinitionResource interface { + ContextAwareResource + ApiDefinition + GetDefinition() ApiDefinition + PopulateIDs(context Context) +} + +// +k8s:deepcopy-gen=false +type ApplicationResource interface { ContextAwareResource - Version() ApiDefinitionVersion - OrgID() string - EnvID() string } // +k8s:deepcopy-gen=false @@ -69,7 +80,47 @@ type ContextAwareResource interface { Resource ContextRef() ResourceRef HasContext() bool - ID() string + GetID() string + GetOrgID() string + GetEnvID() string +} + +// +k8s:deepcopy-gen=false +type SecretAware interface { + GetSecretRef() ResourceRef + HasSecretRef() bool +} + +// +k8s:deepcopy-gen=false +type Context interface { + SecretAware + GetURL() string + GetEnvID() string + GetOrgID() string + HasAuthentication() bool + GetAuth() Auth +} + +// +k8s:deepcopy-gen=false +type ContextResource interface { + Context + Resource +} + +// +k8s:deepcopy-gen=false +type Auth interface { + GetBearerToken() string + HasCredentials() bool + GetCredentials() BasicAuth + GetSecretRef() ResourceRef + SetCredentials(username, password string) + SetToken(token string) +} + +// +k8s:deepcopy-gen=false +type BasicAuth interface { + GetUsername() string + GetPassword() string } // +k8s:deepcopy-gen=false diff --git a/test/integration/admission/apiV2_create_withContext_andUnknownContext_test.go b/test/integration/admission/apiV2_create_withContext_andUnknownContext_test.go new file mode 100644 index 000000000..db0f08cdf --- /dev/null +++ b/test/integration/admission/apiV2_create_withContext_andUnknownContext_test.go @@ -0,0 +1,56 @@ +// 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 admission + +import ( + "context" + "fmt" + + v2 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v2" + "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/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Validate create", labels.WithContext, func() { + interval := constants.Interval + ctx := context.Background() + admissionCtrl := v2.AdmissionCtrl{} + + It("should return error on api creation with missing management context", func() { + fixtures := fixture. + Builder(). + WithAPI(constants.ApiWithContextFile). + Build(). + Apply() + + By("checking that application does not pass validation") + + Consistently(func() error { + _, err := admissionCtrl.ValidateCreate(ctx, fixtures.API) + return assert.Equals( + "error", + fmt.Errorf( + "resource [%s] references management context [default/dev-ctx] that doesn't exist in the cluster", + fixtures.API.Name, + ), + err, + ) + }, constants.ConsistentTimeout, interval).ShouldNot(Succeed()) + }) +}) diff --git a/test/integration/admissionwebhook/api_v4_create_withContext_missing_context_webhook_test.go b/test/integration/admission/apiV4_create_withContext_andUnknownContext_test.go similarity index 78% rename from test/integration/admissionwebhook/api_v4_create_withContext_missing_context_webhook_test.go rename to test/integration/admission/apiV4_create_withContext_andUnknownContext_test.go index 04696f6ff..9e14008b2 100644 --- a/test/integration/admissionwebhook/api_v4_create_withContext_missing_context_webhook_test.go +++ b/test/integration/admission/apiV4_create_withContext_andUnknownContext_test.go @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" "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" @@ -27,20 +28,20 @@ import ( "k8s.io/apimachinery/pkg/types" ) -var _ = Describe("Webhook", labels.WithContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate create", labels.WithContext, func() { interval := constants.Interval - ctx := context.Background() + admissionCtrl := v4.AdmissionCtrl{} - It("should get errors for API creation, missing management context", func() { + It("should return error on API creation with missing management context", func() { fixtures := fixture. Builder(). WithAPIv4(constants.ApiV4WithContextFile). Build(). Apply() - By("Check API creation validation") + By("checking that validation fails") + Consistently(func() error { api := new(v1alpha1.ApiV4Definition) if err := manager.Client().Get(ctx, types.NamespacedName{ @@ -50,8 +51,8 @@ var _ = Describe("Webhook", labels.WithContext, func() { return err } - _, err := api.ValidateCreate() + _, err := admissionCtrl.ValidateUpdate(ctx, api, api) return err - }, timeout, interval).ShouldNot(Succeed()) + }, constants.ConsistentTimeout, interval).ShouldNot(Succeed()) }) }) diff --git a/test/integration/admissionwebhook/api_v4_create_withoutContext_invalid_path_webhook_test.go b/test/integration/admission/apiV4_create_withoutContext_andConflictingPath_test.go similarity index 78% rename from test/integration/admissionwebhook/api_v4_create_withoutContext_invalid_path_webhook_test.go rename to test/integration/admission/apiV4_create_withoutContext_andConflictingPath_test.go index 0d87b2194..6725d9c6d 100644 --- a/test/integration/admissionwebhook/api_v4_create_withoutContext_invalid_path_webhook_test.go +++ b/test/integration/admission/apiV4_create_withoutContext_andConflictingPath_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" @@ -20,6 +20,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" "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" @@ -28,20 +29,20 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Webhook", labels.WithContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate create", labels.WithoutContext, func() { interval := constants.Interval - ctx := context.Background() + admissionCtrl := v4.AdmissionCtrl{} - It("should get errors for API creation because of existing path", func() { + It("should return error on API creation with conflicting path", func() { fixtures := fixture. Builder(). WithAPIv4(constants.ApiV4). Build(). Apply() - By("Check API creation validation") + By("checking that API creation does not pass validation") + Eventually(func() error { api := &v1alpha1.ApiV4Definition{ ObjectMeta: metav1.ObjectMeta{ @@ -56,8 +57,8 @@ var _ = Describe("Webhook", labels.WithContext, func() { return err } - _, err := api.ValidateCreate() + _, err := admissionCtrl.ValidateCreate(ctx, api) return err - }, timeout, interval).ShouldNot(Succeed()) + }, constants.EventualTimeout, interval).ShouldNot(Succeed()) }) }) diff --git a/test/integration/admission/apiV4_update_withContext_andUnknownCategory_test.go b/test/integration/admission/apiV4_update_withContext_andUnknownCategory_test.go new file mode 100644 index 000000000..ffdf94407 --- /dev/null +++ b/test/integration/admission/apiV4_update_withContext_andUnknownCategory_test.go @@ -0,0 +1,73 @@ +// 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 admission + +import ( + "context" + + apiV4 "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/v4" + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "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("Validate update", labels.WithContext, func() { + interval := constants.Interval + ctx := context.Background() + admissionCtrl := v4.AdmissionCtrl{} + + It("should return warning on API creation with unknown category", func() { + fixtures := fixture. + Builder(). + WithAPIv4(constants.ApiV4). + WithContext(constants.ContextWithCredentialsFile). + Build(). + Apply() + + By("preparing API for import") + + fixtures.APIv4.Spec.DefinitionContext = apiV4.NewDefaultKubernetesContext() + fixtures.APIv4.PopulateIDs(fixtures.Context) + + By("adding an unknown category to the API") + + unknownCategory := random.GetName() + + fixtures.APIv4.Spec.Categories = []string{unknownCategory} + + By("checking that API validation returns warnings") + + Eventually(func() error { + warnings, err := admissionCtrl.ValidateUpdate(ctx, fixtures.APIv4, fixtures.APIv4) + if err != nil { + return err + } + if err = assert.SliceOfSize("warnings", warnings, 1); err != nil { + return err + } + return assert.Equals( + "warning", + errors.NewWarning("category [%s] is not defined in environment [DEFAULT]", unknownCategory).Error(), + warnings[0], + ) + }, constants.EventualTimeout, interval).Should(Succeed()) + }) +}) diff --git a/test/integration/admissionwebhook/api_v4_update_withContext_missing_context_webhook_test.go b/test/integration/admission/apiV4_update_withContext_andUnknownContext_test.go similarity index 77% rename from test/integration/admissionwebhook/api_v4_update_withContext_missing_context_webhook_test.go rename to test/integration/admission/apiV4_update_withContext_andUnknownContext_test.go index 9d573b8c1..758c5769a 100644 --- a/test/integration/admissionwebhook/api_v4_update_withContext_missing_context_webhook_test.go +++ b/test/integration/admission/apiV4_update_withContext_andUnknownContext_test.go @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" "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" @@ -27,22 +28,23 @@ import ( "k8s.io/apimachinery/pkg/types" ) -var _ = Describe("Webhook", labels.WithContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate update", labels.WithContext, func() { interval := constants.Interval - ctx := context.Background() + admissionCtrl := v4.AdmissionCtrl{} - It("should get errors for API update, missing management context", func() { + It("should return error on API update with missing management context", func() { fixtures := fixture. Builder(). WithAPIv4(constants.ApiV4WithContextFile). Build(). Apply() - By("Check API update validation") + By("checking that API update does not pass validation") + Consistently(func() error { api := new(v1alpha1.ApiV4Definition) + if err := manager.Client().Get(ctx, types.NamespacedName{ Name: fixtures.APIv4.Name, Namespace: fixtures.APIv4.Namespace, @@ -50,8 +52,8 @@ var _ = Describe("Webhook", labels.WithContext, func() { return err } - _, err := api.ValidateUpdate(nil) + _, err := admissionCtrl.ValidateUpdate(ctx, api, api) return err - }, timeout, interval).ShouldNot(Succeed()) + }, constants.ConsistentTimeout, interval).ShouldNot(Succeed()) }) }) diff --git a/test/integration/admission/apiV4_update_withContext_andUnknownMember_test.go b/test/integration/admission/apiV4_update_withContext_andUnknownMember_test.go new file mode 100644 index 000000000..7d93cdfce --- /dev/null +++ b/test/integration/admission/apiV4_update_withContext_andUnknownMember_test.go @@ -0,0 +1,79 @@ +// 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 admission + +import ( + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" + apiV4 "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/v4" + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" + "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("Validate create", labels.WithContext, func() { + interval := constants.Interval + ctx := context.Background() + admissionCtrl := v4.AdmissionCtrl{} + + It("should return warning on API creation with unknown category", func() { + fixtures := fixture. + Builder(). + WithAPIv4(constants.ApiV4). + WithContext(constants.ContextWithCredentialsFile). + Build(). + Apply() + + By("preparing API for import") + + fixtures.APIv4.Spec.DefinitionContext = apiV4.NewDefaultKubernetesContext() + fixtures.APIv4.PopulateIDs(fixtures.Context) + + By("adding an unknown member to the API") + + unknownMemberName := random.GetName() + + fixtures.APIv4.Spec.Members = []*base.Member{ + base.NewGraviteeMember(unknownMemberName, "REVIEWER"), + } + + By("checking that API validation returns warnings") + + Eventually(func() error { + warnings, err := admissionCtrl.ValidateUpdate(ctx, fixtures.APIv4, fixtures.APIv4) + if err != nil { + return err + } + if err = assert.SliceOfSize("warnings", warnings, 1); err != nil { + return err + } + return assert.Equals( + "warning", + errors.NewWarning( + "Member [%s] of source [gravitee] could not be found in organization [DEFAULT]", + unknownMemberName, + ).Error(), + warnings[0], + ) + }, constants.EventualTimeout, interval).Should(Succeed()) + }) +}) diff --git a/test/integration/admissionwebhook/api_v4_update_withoutContext_invalid_path_webhook_test.go b/test/integration/admission/apiV4_update_withoutContext_andConflictingPath_test.go similarity index 77% rename from test/integration/admissionwebhook/api_v4_update_withoutContext_invalid_path_webhook_test.go rename to test/integration/admission/apiV4_update_withoutContext_andConflictingPath_test.go index 5b9596d57..4debcac57 100644 --- a/test/integration/admissionwebhook/api_v4_update_withoutContext_invalid_path_webhook_test.go +++ b/test/integration/admission/apiV4_update_withoutContext_andConflictingPath_test.go @@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( + "context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + v4 "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/api/v4" "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" @@ -25,11 +28,12 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Webhook", labels.WithContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate update", labels.WithoutContext, func() { interval := constants.Interval + ctx := context.Background() + admissionCtrl := v4.AdmissionCtrl{} - It("should get errors for API update because of existing path", func() { + It("should return error on API update with conflicting path", func() { fixtures := fixture. Builder(). WithAPIv4(constants.ApiV4). @@ -44,11 +48,9 @@ var _ = Describe("Webhook", labels.WithContext, func() { Namespace: fixtures.APIv4.Namespace, }, } - fixtures.APIv4.Spec.DeepCopyInto(&api.Spec) - - _, err := api.ValidateUpdate(nil) + _, err := admissionCtrl.ValidateUpdate(ctx, api, api) return err - }, timeout, interval).ShouldNot(Succeed()) + }, constants.EventualTimeout, interval).ShouldNot(Succeed()) }) }) diff --git a/test/integration/admission/application_create_withContext_andUnknownContext_test.go b/test/integration/admission/application_create_withContext_andUnknownContext_test.go new file mode 100644 index 000000000..d3852c616 --- /dev/null +++ b/test/integration/admission/application_create_withContext_andUnknownContext_test.go @@ -0,0 +1,56 @@ +// 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 admission + +import ( + "context" + "fmt" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/application" + "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/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Validate create", labels.WithContext, func() { + interval := constants.Interval + ctx := context.Background() + admissionCtrl := application.AdmissionCtrl{} + + It("should return error on application creation with missing management context", func() { + fixtures := fixture. + Builder(). + WithApplication(constants.Application). + Build(). + Apply() + + By("checking that application does not pass validation") + + Consistently(func() error { + _, err := admissionCtrl.ValidateCreate(ctx, fixtures.Application) + return assert.Equals( + "error", + fmt.Errorf( + "resource [%s] references management context [default/dev-ctx] that doesn't exist in the cluster", + fixtures.Application.Name, + ), + err, + ) + }, constants.ConsistentTimeout, interval).ShouldNot(Succeed()) + }) +}) diff --git a/test/integration/admissionwebhook/managementcontext_create_missing_secret_webhook_test.go b/test/integration/admission/managementcontext_create_withBadCredentials_test.go similarity index 58% rename from test/integration/admissionwebhook/managementcontext_create_missing_secret_webhook_test.go rename to test/integration/admission/managementcontext_create_withBadCredentials_test.go index 099a8dc13..075d705c0 100644 --- a/test/integration/admissionwebhook/managementcontext_create_missing_secret_webhook_test.go +++ b/test/integration/admission/managementcontext_create_withBadCredentials_test.go @@ -12,55 +12,50 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" - "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" - "k8s.io/apimachinery/pkg/types" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" "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/internal/admission/mctx" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/errors" ) -var _ = Describe("Webhook", labels.WithoutContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate create", labels.WithContext, func() { interval := constants.Interval + ctx := context.Background() + admissionCtrl := mctx.AdmissionCtrl{} + + It("should return error if secret is missing", func() { + + By("setting invalid credentials onto context") - It("Show throws error secret is missing", func() { - cli := manager.Client() fixtures := fixture.Builder(). - WithContext(constants.ContextWithSecretFile). + WithContext(constants.ContextWithBadCredentialsFile). Build(). Apply() - ctx := new(v1alpha1.ManagementContext) - Eventually(func() error { - err := cli.Get(context.Background(), types.NamespacedName{ - Namespace: fixtures.Context.Namespace, - Name: fixtures.Context.Name, - }, ctx) - - if err != nil { - return err - } - return nil - }).Should(Succeed()) + By("validating the context") Consistently(func() error { - warnings, err := ctx.ValidateCreate() - if len(warnings) != 0 { - return nil - } - - return err - }, timeout, interval).ShouldNot(Succeed()) + _, err := admissionCtrl.ValidateCreate(ctx, fixtures.Context) + return assert.Equals( + "error", + errors.NewSevere( + "bad credentials for context [%s]", + fixtures.Context.Name, + ), + err, + ) + }, constants.ConsistentTimeout, interval).Should(Succeed()) }) }) diff --git a/test/integration/admissionwebhook/api_v2_mutate_withContext_withoutNamespace.go b/test/integration/admission/managementcontext_create_withEnvironmentNotFound_test.go similarity index 57% rename from test/integration/admissionwebhook/api_v2_mutate_withContext_withoutNamespace.go rename to test/integration/admission/managementcontext_create_withEnvironmentNotFound_test.go index 4af227e3a..027091cda 100644 --- a/test/integration/admissionwebhook/api_v2_mutate_withContext_withoutNamespace.go +++ b/test/integration/admission/managementcontext_create_withEnvironmentNotFound_test.go @@ -12,34 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( - "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "context" + + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" - "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" + + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" ) -var _ = Describe("Mutate", labels.WithContext, func() { - It("should set context namespace to api namespace if empty", func() { - fixtures := fixture. - Builder(). - WithAPI(constants.ApiWithContextFile). - WithContext(constants.ContextWithCredentialsFile). - Build() +var _ = Describe("Validate create", labels.WithContext, func() { + interval := constants.Interval + ctx := context.Background() + admissionCtrl := mctx.AdmissionCtrl{} - By("removing namespace from context reference") + It("should return error if secret is missing", func() { - fixtures.API.Spec.Context.Namespace = "" + By("setting unknown environment onto context") - By("applying defaults") + fixtures := fixture.Builder(). + WithContext(constants.ContextWithCredentialsFile). + Build() - Expect(fixtures.API.Namespace).ToNot(BeEmpty()) + fixtures.Context.Spec.EnvID = "unknown" - fixtures.API.Default() + By("validating the context") - Expect(fixtures.API.Spec.Context.Namespace).To(Equal(fixtures.API.Namespace)) + Consistently(func() error { + _, err := admissionCtrl.ValidateCreate(ctx, fixtures.Context) + return assert.NotNil("error", err) + }, constants.ConsistentTimeout, interval).Should(Succeed()) }) }) diff --git a/test/integration/admissionwebhook/api_v4_mutate_withContext_withoutNamespace.go b/test/integration/admission/managementcontext_create_withMissingSecret_test.go similarity index 62% rename from test/integration/admissionwebhook/api_v4_mutate_withContext_withoutNamespace.go rename to test/integration/admission/managementcontext_create_withMissingSecret_test.go index e28272bc3..e1dd99944 100644 --- a/test/integration/admissionwebhook/api_v4_mutate_withContext_withoutNamespace.go +++ b/test/integration/admission/managementcontext_create_withMissingSecret_test.go @@ -12,34 +12,35 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( - "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "context" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" - "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" -) - -var _ = Describe("Mutate", labels.WithContext, func() { - It("should set context namespace to api namespace if empty", func() { - fixtures := fixture. - Builder(). - WithAPIv4(constants.ApiV4WithContextFile). - WithContext(constants.ContextWithCredentialsFile). - Build() - - By("removing namespace from context reference") - fixtures.APIv4.Spec.Context.Namespace = "" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/constants" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/labels" - By("applying defaults") + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" +) - Expect(fixtures.APIv4.Namespace).ToNot(BeEmpty()) +var _ = Describe("Validate create", labels.WithContext, func() { + interval := constants.Interval + ctx := context.Background() + admissionCtrl := mctx.AdmissionCtrl{} - fixtures.APIv4.Default() + It("should return error if secret is missing", func() { + fixtures := fixture.Builder(). + WithContext(constants.ContextWithSecretFile). + Build() - Expect(fixtures.APIv4.Spec.Context.Namespace).To(Equal(fixtures.APIv4.Namespace)) + Consistently(func() error { + _, err := admissionCtrl.ValidateCreate(ctx, fixtures.Context) + return err + }, constants.ConsistentTimeout, interval).Should(HaveOccurred()) }) }) diff --git a/test/integration/admissionwebhook/managementcontext_create_unavailable_apim_webhook_test.go b/test/integration/admission/managementcontext_create_withNetworkError_test.go similarity index 71% rename from test/integration/admissionwebhook/managementcontext_create_unavailable_apim_webhook_test.go rename to test/integration/admission/managementcontext_create_withNetworkError_test.go index 05022b322..25b590bcf 100644 --- a/test/integration/admissionwebhook/managementcontext_create_unavailable_apim_webhook_test.go +++ b/test/integration/admission/managementcontext_create_withNetworkError_test.go @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" - "errors" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/random" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -32,12 +33,13 @@ import ( "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/manager" ) -var _ = Describe("Webhook", labels.WithoutContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate create", labels.WithContext, func() { interval := constants.Interval + ctx := context.Background() + admissionCtrl := mctx.AdmissionCtrl{} + cli := manager.Client() - It("Show gives warning when APIM is not accessible", func() { - cli := manager.Client() + It("should return warning if APIM is not accessible", func() { mCtx := &v1alpha1.ManagementContext{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ @@ -47,8 +49,8 @@ var _ = Describe("Webhook", labels.WithoutContext, func() { Spec: v1alpha1.ManagementContextSpec{ Context: &management.Context{ BaseUrl: "https://gko.example.com", - EnvId: "DEFAULT", - OrgId: "DEFAULT", + EnvID: "DEFAULT", + OrgID: "DEFAULT", Auth: &management.Auth{ BearerToken: "test", }, @@ -58,26 +60,16 @@ var _ = Describe("Webhook", labels.WithoutContext, func() { Expect(cli.Create(context.Background(), mCtx)).To(Succeed()) - ctx := new(v1alpha1.ManagementContext) Eventually(func() error { - err := cli.Get(context.Background(), types.NamespacedName{ + return cli.Get(context.Background(), types.NamespacedName{ Namespace: mCtx.Namespace, Name: mCtx.Name, - }, ctx) - - if err != nil { - return err - } - return nil - }).Should(Succeed()) + }, mCtx) + }, constants.EventualTimeout, interval).Should(Succeed()) Consistently(func() error { - warnings, _ := ctx.ValidateCreate() - if len(warnings) != 1 { - return nil - } - - return errors.New(warnings[0]) - }, timeout, interval).ShouldNot(Succeed()) + warnings, _ := admissionCtrl.ValidateCreate(ctx, mCtx) + return assert.SliceOfSize("warnings", warnings, 1) + }, constants.ConsistentTimeout, interval).Should(Succeed()) }) }) diff --git a/test/integration/admissionwebhook/managementcontext_update_missing_secret_webhook_test.go b/test/integration/admission/managementcontext_update_withMissingSecret_test.go similarity index 70% rename from test/integration/admissionwebhook/managementcontext_update_missing_secret_webhook_test.go rename to test/integration/admission/managementcontext_update_withMissingSecret_test.go index 1f4d58f76..3f07723b7 100644 --- a/test/integration/admissionwebhook/managementcontext_update_missing_secret_webhook_test.go +++ b/test/integration/admission/managementcontext_update_withMissingSecret_test.go @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/fixture" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" @@ -30,37 +31,29 @@ import ( "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/manager" ) -var _ = Describe("Webhook", labels.WithoutContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate update", labels.WithContext, func() { interval := constants.Interval + admissionCtrl := mctx.AdmissionCtrl{} + ctx := context.Background() + cli := manager.Client() - It("Show throws error secret is missing", func() { - cli := manager.Client() + It("should return error if secret is missing", func() { fixtures := fixture.Builder(). WithContext(constants.ContextWithSecretFile). Build(). Apply() - ctx := new(v1alpha1.ManagementContext) + mCtx := new(v1alpha1.ManagementContext) Eventually(func() error { - err := cli.Get(context.Background(), types.NamespacedName{ + return cli.Get(context.Background(), types.NamespacedName{ Namespace: fixtures.Context.Namespace, Name: fixtures.Context.Name, - }, ctx) - - if err != nil { - return err - } - return nil - }).Should(Succeed()) + }, mCtx) + }, constants.EventualTimeout, interval).Should(Succeed()) Consistently(func() error { - warnings, err := ctx.ValidateUpdate(nil) - if len(warnings) != 0 { - return nil - } - + _, err := admissionCtrl.ValidateCreate(ctx, mCtx) return err - }, timeout, interval).ShouldNot(Succeed()) + }, constants.ConsistentTimeout, interval).ShouldNot(Succeed()) }) }) diff --git a/test/integration/admissionwebhook/managementcontext_update_unavailable_apim_webhook_test.go b/test/integration/admission/managementcontext_update_withNetworkError_test.go similarity index 71% rename from test/integration/admissionwebhook/managementcontext_update_unavailable_apim_webhook_test.go rename to test/integration/admission/managementcontext_update_withNetworkError_test.go index eb872e3b1..bedbab89a 100644 --- a/test/integration/admissionwebhook/managementcontext_update_unavailable_apim_webhook_test.go +++ b/test/integration/admission/managementcontext_update_withNetworkError_test.go @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package admission import ( "context" - "errors" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/management" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" + "github.com/gravitee-io/gravitee-kubernetes-operator/internal/admission/mctx" + "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/assert" "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/random" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -32,12 +33,13 @@ import ( "github.com/gravitee-io/gravitee-kubernetes-operator/test/internal/integration/manager" ) -var _ = Describe("Webhook", labels.WithoutContext, func() { - timeout := constants.EventualTimeout / 10 +var _ = Describe("Validate update", labels.WithContext, func() { interval := constants.Interval + admissionCtrl := mctx.AdmissionCtrl{} + ctx := context.Background() + cli := manager.Client() - It("Show gives warning when APIM is not accessible", func() { - cli := manager.Client() + It("should return warnings if APIM is not accessible", func() { mCtx := &v1alpha1.ManagementContext{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ @@ -47,8 +49,8 @@ var _ = Describe("Webhook", labels.WithoutContext, func() { Spec: v1alpha1.ManagementContextSpec{ Context: &management.Context{ BaseUrl: "https://gko.example.com", - EnvId: "DEFAULT", - OrgId: "DEFAULT", + EnvID: "DEFAULT", + OrgID: "DEFAULT", Auth: &management.Auth{ BearerToken: "test", }, @@ -58,26 +60,16 @@ var _ = Describe("Webhook", labels.WithoutContext, func() { Expect(cli.Create(context.Background(), mCtx)).To(Succeed()) - ctx := new(v1alpha1.ManagementContext) Eventually(func() error { - err := cli.Get(context.Background(), types.NamespacedName{ + return cli.Get(context.Background(), types.NamespacedName{ Namespace: mCtx.Namespace, Name: mCtx.Name, - }, ctx) - - if err != nil { - return err - } - return nil - }).Should(Succeed()) + }, mCtx) + }, constants.EventualTimeout, interval).Should(Succeed()) Consistently(func() error { - warnings, _ := ctx.ValidateUpdate(nil) - if len(warnings) != 1 { - return nil - } - - return errors.New(warnings[0]) - }, timeout, interval).ShouldNot(Succeed()) + warnings, _ := admissionCtrl.ValidateUpdate(ctx, mCtx, mCtx) + return assert.SliceOfSize("warnings", warnings, 1) + }, constants.ConsistentTimeout, interval).ShouldNot(Succeed()) }) }) diff --git a/test/integration/admission/suite_test.go b/test/integration/admission/suite_test.go new file mode 100644 index 000000000..14812be75 --- /dev/null +++ b/test/integration/admission/suite_test.go @@ -0,0 +1,38 @@ +// 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 admission + +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, "Admission Suite") +} + +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/admissionwebhook/application_mutate_withContext_withoutNamespace.go b/test/integration/admissionwebhook/application_mutate_withContext_withoutNamespace.go deleted file mode 100644 index 276c8d38c..000000000 --- a/test/integration/admissionwebhook/application_mutate_withContext_withoutNamespace.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 admissionwebhook - -import ( - "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/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Mutate", labels.WithContext, func() { - It("should set context namespace to application namespace if empty", func() { - fixtures := fixture. - Builder(). - WithApplication(constants.Application). - WithContext(constants.ContextWithCredentialsFile). - Build() - - By("removing namespace from context reference") - - fixtures.Application.Spec.Context.Namespace = "" - - By("applying defaults") - - Expect(fixtures.Application.Namespace).ToNot(BeEmpty()) - - fixtures.Application.Default() - - Expect(fixtures.Application.Spec.Context.Namespace).To(Equal(fixtures.Application.Namespace)) - }) -}) diff --git a/test/integration/apidefinition/create_withContext_andMardownPage_test.go b/test/integration/apidefinition/create_withContext_andMardownPage_test.go index bc0247316..6d3c8e0f0 100644 --- a/test/integration/apidefinition/create_withContext_andMardownPage_test.go +++ b/test/integration/apidefinition/create_withContext_andMardownPage_test.go @@ -42,12 +42,12 @@ var _ = Describe("Create", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.API.Status.ID + apiID := fixtures.API.Status.ID By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId) + pages, err := apim.Pages.FindByAPI(apiID) if err != nil { return err } @@ -61,7 +61,7 @@ var _ = Describe("Create", labels.WithContext, func() { Expect(markdown).ToNot(BeNil()) Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId, model.NewPageQuery().WithType("MARKDOWN")) + pages, err := apim.Pages.FindByAPI(apiID, model.NewPageQuery().WithType("MARKDOWN")) if err != nil { return err } diff --git a/test/integration/apidefinition/create_withContext_MemberWithoutRole_test.go b/test/integration/apidefinition/create_withContext_andMemberWithoutRole_test.go similarity index 91% rename from test/integration/apidefinition/create_withContext_MemberWithoutRole_test.go rename to test/integration/apidefinition/create_withContext_andMemberWithoutRole_test.go index 572075b9e..a4e8eb514 100644 --- a/test/integration/apidefinition/create_withContext_MemberWithoutRole_test.go +++ b/test/integration/apidefinition/create_withContext_andMemberWithoutRole_test.go @@ -16,7 +16,7 @@ package apidefinition import ( "context" - "sort" + "strings" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" @@ -72,13 +72,13 @@ var _ = Describe("Update", labels.WithContext, func() { exportedMembers := apiExport.Spec.Members - By("sorting exported API members by source") - - sort.Slice(exportedMembers, func(i, j int) bool { - return exportedMembers[i].Source < exportedMembers[j].Source - }) - - return assert.Equals("members", expectedMembers, exportedMembers) + return assert.SliceEqualsSorted( + "members", + expectedMembers, exportedMembers, + func(a, b *base.Member) int { + return strings.Compare(a.Source+a.SourceID, b.Source+b.SourceID) + }, + ) }, timeout, interval).Should(Succeed(), fixtures.API.Name) }) }) diff --git a/test/integration/apidefinition/create_withContext_andPageWithACL_test.go b/test/integration/apidefinition/create_withContext_andPageWithACL_test.go index 451cc2d3d..2fac22946 100644 --- a/test/integration/apidefinition/create_withContext_andPageWithACL_test.go +++ b/test/integration/apidefinition/create_withContext_andPageWithACL_test.go @@ -46,7 +46,7 @@ var _ = Describe("Create", labels.WithContext, func() { acl := []base.AccessControl{ { - ReferenceId: groupName, + ReferenceID: groupName, ReferenceType: "GROUP", }, } diff --git a/test/integration/apidefinition/create_withContext_andSwaggerHTTPFetcher_test.go b/test/integration/apidefinition/create_withContext_andSwaggerHTTPFetcher_test.go index 73f5a184f..8b2f8cfcb 100644 --- a/test/integration/apidefinition/create_withContext_andSwaggerHTTPFetcher_test.go +++ b/test/integration/apidefinition/create_withContext_andSwaggerHTTPFetcher_test.go @@ -42,12 +42,12 @@ var _ = Describe("Create", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.API.Status.ID + apiID := fixtures.API.Status.ID By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId) + pages, err := apim.Pages.FindByAPI(apiID) if err != nil { return err } @@ -57,7 +57,7 @@ var _ = Describe("Create", labels.WithContext, func() { By("checking swagger content in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId, model.NewPageQuery().WithType("SWAGGER")) + pages, err := apim.Pages.FindByAPI(apiID, model.NewPageQuery().WithType("SWAGGER")) if err != nil { return err } diff --git a/test/integration/apidefinition/create_withContext_fromExport_test.go b/test/integration/apidefinition/create_withContext_fromExport_test.go index bc09db2c6..8da1bae65 100644 --- a/test/integration/apidefinition/create_withContext_fromExport_test.go +++ b/test/integration/apidefinition/create_withContext_fromExport_test.go @@ -41,7 +41,7 @@ var _ = Describe("Create", labels.WithContext, func() { It("should update existing api in management API", func() { fixtures := fixture.Builder(). WithContext(constants.ContextWithSecretFile). - WithAPI(constants.ApiWithIds). + WithAPI(constants.ApiWithIDs). Build() By("creating API in management api") diff --git a/test/integration/apidefinition/subscribe_withContext_test.go b/test/integration/apidefinition/subscribe_withContext_test.go index 6d18de10a..1e01297e4 100644 --- a/test/integration/apidefinition/subscribe_withContext_test.go +++ b/test/integration/apidefinition/subscribe_withContext_test.go @@ -69,16 +69,16 @@ var _ = Describe("Subscribe", labels.WithContext, func() { api, err := apim.APIs.GetByID(fixtures.API.Status.ID) Expect(err).ToNot(HaveOccurred()) Expect(api.Plans).ToNot(BeEmpty()) - planID := api.Plans[0].Id + planID := api.Plans[0].ID By("calling rest API expecting to application to subscribe to API") - subscription, err := apim.Subscriptions.Subscribe(fixtures.API.Status.ID, app.Id, planID) + subscription, err := apim.Subscriptions.Subscribe(fixtures.API.Status.ID, app.ID, planID) Expect(err).ToNot(HaveOccurred()) By("calling rest API expecting to find subscription API key") - keys, err := apim.Subscriptions.GetApiKeys(fixtures.API.Status.ID, subscription.Id) + keys, err := apim.Subscriptions.GetApiKeys(fixtures.API.Status.ID, subscription.ID) Expect(err).ToNot(HaveOccurred()) Expect(keys).ToNot(BeEmpty()) key := keys[0].Key diff --git a/test/integration/apidefinition/update_withContext_andMardownPage_test.go b/test/integration/apidefinition/update_withContext_andMardownPage_test.go index 623683f6e..8b94ab6c5 100644 --- a/test/integration/apidefinition/update_withContext_andMardownPage_test.go +++ b/test/integration/apidefinition/update_withContext_andMardownPage_test.go @@ -43,7 +43,7 @@ var _ = Describe("Update", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.API.Status.ID + apiID := fixtures.API.Status.ID By("updating markdown content") @@ -58,7 +58,7 @@ var _ = Describe("Update", labels.WithContext, func() { By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId) + pages, err := apim.Pages.FindByAPI(apiID) if err != nil { return err } @@ -70,7 +70,7 @@ var _ = Describe("Update", labels.WithContext, func() { Expect(markdown).ToNot(BeNil()) Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId, model.NewPageQuery().WithType("MARKDOWN")) + pages, err := apim.Pages.FindByAPI(apiID, model.NewPageQuery().WithType("MARKDOWN")) if err != nil { return err } diff --git a/test/integration/apidefinition/update_withContext_changingMemberRole_test.go b/test/integration/apidefinition/update_withContext_changingMemberRole_test.go index 8baf0da4e..f0f5fea06 100644 --- a/test/integration/apidefinition/update_withContext_changingMemberRole_test.go +++ b/test/integration/apidefinition/update_withContext_changingMemberRole_test.go @@ -16,6 +16,7 @@ package apidefinition import ( "context" + "strings" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" @@ -88,7 +89,13 @@ var _ = Describe("Update", labels.WithContext, func() { if err != nil { return err } - return assert.Equals("members", expectedMembers, export.Spec.Members) + return assert.SliceEqualsSorted( + "members", + expectedMembers, export.Spec.Members, + func(a, b *base.Member) int { + return strings.Compare(a.SourceID, b.SourceID) + }, + ) }, timeout, interval).Should(Succeed(), fixtures.API.Name) }) }) diff --git a/test/integration/apidefinition/update_withContext_deletingPage_test.go b/test/integration/apidefinition/update_withContext_deletingPage_test.go index e6df31d0b..63b0fddac 100644 --- a/test/integration/apidefinition/update_withContext_deletingPage_test.go +++ b/test/integration/apidefinition/update_withContext_deletingPage_test.go @@ -43,12 +43,12 @@ var _ = Describe("Update", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.API.Status.ID + apiID := fixtures.API.Status.ID By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId) + pages, err := apim.Pages.FindByAPI(apiID) if err != nil { return err } @@ -66,7 +66,7 @@ var _ = Describe("Update", labels.WithContext, func() { By("checking that markdown page has been deleted in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId, model.NewPageQuery().WithType("MARKDOWN")) + pages, err := apim.Pages.FindByAPI(apiID, model.NewPageQuery().WithType("MARKDOWN")) if err != nil { return err } diff --git a/test/integration/apidefinition/update_withContext_removingMember_test.go b/test/integration/apidefinition/update_withContext_removingMember_test.go index 75daedaae..ce2dfc30a 100644 --- a/test/integration/apidefinition/update_withContext_removingMember_test.go +++ b/test/integration/apidefinition/update_withContext_removingMember_test.go @@ -16,6 +16,7 @@ package apidefinition import ( "context" + "strings" "github.com/gravitee-io/gravitee-kubernetes-operator/api/model/api/base" "github.com/gravitee-io/gravitee-kubernetes-operator/internal/apim/model" @@ -71,7 +72,13 @@ var _ = Describe("Update", labels.WithContext, func() { if err != nil { return err } - return assert.Equals("members", []*base.Member{primaryOwner, saMember}, export.Spec.Members) + return assert.SliceEqualsSorted( + "members", + []*base.Member{primaryOwner, saMember}, export.Spec.Members, + func(a, b *base.Member) int { + return strings.Compare(a.SourceID, b.SourceID) + }, + ) }, timeout, interval).Should(Succeed(), fixtures.API.Name) By("removing service account from API members") diff --git a/test/integration/apidefinition/v4-create_withContext_andMardownPage_test.go b/test/integration/apidefinition/v4-create_withContext_andMardownPage_test.go index c604d60bc..1c5bb6f61 100644 --- a/test/integration/apidefinition/v4-create_withContext_andMardownPage_test.go +++ b/test/integration/apidefinition/v4-create_withContext_andMardownPage_test.go @@ -42,12 +42,12 @@ var _ = Describe("Create", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.APIv4.Status.ID + apiID := fixtures.APIv4.Status.ID By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId) + pages, err := apim.Pages.FindByAPIV4(apiID) if err != nil { return err } @@ -61,7 +61,7 @@ var _ = Describe("Create", labels.WithContext, func() { Expect(markdown).ToNot(BeNil()) Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId, model.NewPageQuery().WithType("MARKDOWN")) + pages, err := apim.Pages.FindByAPIV4(apiID, model.NewPageQuery().WithType("MARKDOWN")) if err != nil { return err } diff --git a/test/integration/apidefinition/v4-update_withContext_andMardownPage_test.go b/test/integration/apidefinition/v4-update_withContext_andMardownPage_test.go index aea0b7a89..9e50e11b4 100644 --- a/test/integration/apidefinition/v4-update_withContext_andMardownPage_test.go +++ b/test/integration/apidefinition/v4-update_withContext_andMardownPage_test.go @@ -43,7 +43,7 @@ var _ = Describe("Update", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.APIv4.Status.ID + apiID := fixtures.APIv4.Status.ID By("updating markdown content") @@ -58,7 +58,7 @@ var _ = Describe("Update", labels.WithContext, func() { By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId) + pages, err := apim.Pages.FindByAPIV4(apiID) if err != nil { return err } @@ -70,7 +70,7 @@ var _ = Describe("Update", labels.WithContext, func() { Expect(markdown).ToNot(BeNil()) Eventually(func() error { - pages, err := apim.Pages.FindByAPI(apiId, model.NewPageQuery().WithType("MARKDOWN")) + pages, err := apim.Pages.FindByAPI(apiID, model.NewPageQuery().WithType("MARKDOWN")) if err != nil { return err } diff --git a/test/integration/apidefinition/v4-update_withContext_deletingPage_test.go b/test/integration/apidefinition/v4-update_withContext_deletingPage_test.go index a2f4301c3..6234c3ad2 100644 --- a/test/integration/apidefinition/v4-update_withContext_deletingPage_test.go +++ b/test/integration/apidefinition/v4-update_withContext_deletingPage_test.go @@ -43,12 +43,12 @@ var _ = Describe("Update", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.APIv4.Status.ID + apiID := fixtures.APIv4.Status.ID By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId) + pages, err := apim.Pages.FindByAPIV4(apiID) if err != nil { return err } @@ -66,7 +66,7 @@ var _ = Describe("Update", labels.WithContext, func() { By("checking that markdown page has been deleted in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId, model.NewPageQuery().WithType("MARKDOWN")) + pages, err := apim.Pages.FindByAPIV4(apiID, model.NewPageQuery().WithType("MARKDOWN")) if err != nil { return err } diff --git a/test/integration/apidefinition/v4_create_withContext_andSwaggerHTTPFetcher_test.go b/test/integration/apidefinition/v4_create_withContext_andSwaggerHTTPFetcher_test.go index 321df8116..4b9e8979a 100644 --- a/test/integration/apidefinition/v4_create_withContext_andSwaggerHTTPFetcher_test.go +++ b/test/integration/apidefinition/v4_create_withContext_andSwaggerHTTPFetcher_test.go @@ -42,12 +42,12 @@ var _ = Describe("Create", labels.WithContext, func() { Apply() apim := apim.NewClient(ctx) - apiId := fixtures.APIv4.Status.ID + apiID := fixtures.APIv4.Status.ID By("checking pages number in APIM") Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId) + pages, err := apim.Pages.FindByAPIV4(apiID) if err != nil { return err } @@ -60,7 +60,7 @@ var _ = Describe("Create", labels.WithContext, func() { Expect(swagger).ToNot(BeNil()) Eventually(func() error { - pages, err := apim.Pages.FindByAPIV4(apiId, model.NewPageQuery().WithType("SWAGGER")) + pages, err := apim.Pages.FindByAPIV4(apiID, model.NewPageQuery().WithType("SWAGGER")) if err != nil { return err } diff --git a/test/integration/apidefinition/v4_subscribe_withContext_test.go b/test/integration/apidefinition/v4_subscribe_withContext_test.go index 49471ece0..369357908 100644 --- a/test/integration/apidefinition/v4_subscribe_withContext_test.go +++ b/test/integration/apidefinition/v4_subscribe_withContext_test.go @@ -71,12 +71,12 @@ var _ = Describe("Subscribe", labels.WithContext, func() { By("calling rest API expecting to application to subscribe to API") - subscription, err := apim.Subscriptions.Subscribe(fixtures.APIv4.Status.ID, app.Id, planID) + subscription, err := apim.Subscriptions.Subscribe(fixtures.APIv4.Status.ID, app.ID, planID) Expect(err).ToNot(HaveOccurred()) By("calling rest API expecting to find subscription API key") - keys, err := apim.Subscriptions.GetApiKeys(fixtures.APIv4.Status.ID, subscription.Id) + keys, err := apim.Subscriptions.GetApiKeys(fixtures.APIv4.Status.ID, subscription.ID) Expect(err).ToNot(HaveOccurred()) Expect(keys).ToNot(BeEmpty()) key := keys[0].Key diff --git a/test/integration/admissionwebhook/suite_test.go b/test/integration/webhook/suite_test.go similarity index 97% rename from test/integration/admissionwebhook/suite_test.go rename to test/integration/webhook/suite_test.go index 4f7063a5b..57460e09d 100644 --- a/test/integration/admissionwebhook/suite_test.go +++ b/test/integration/webhook/suite_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package webhook import ( "testing" diff --git a/test/integration/admissionwebhook/webhook_generate_secretes_withoutContext.go b/test/integration/webhook/webhook_generate_secretes_withoutContext.go similarity index 99% rename from test/integration/admissionwebhook/webhook_generate_secretes_withoutContext.go rename to test/integration/webhook/webhook_generate_secretes_withoutContext.go index 4c33f690c..b95a8f1ab 100644 --- a/test/integration/admissionwebhook/webhook_generate_secretes_withoutContext.go +++ b/test/integration/webhook/webhook_generate_secretes_withoutContext.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package webhook import ( "bytes" diff --git a/test/integration/admissionwebhook/webhook_patch_admission_webhook_configuration_withoutContext.go b/test/integration/webhook/webhook_patch_admission_webhook_configuration_withoutContext.go similarity index 99% rename from test/integration/admissionwebhook/webhook_patch_admission_webhook_configuration_withoutContext.go rename to test/integration/webhook/webhook_patch_admission_webhook_configuration_withoutContext.go index e6ba4a9a5..e1597e8dd 100644 --- a/test/integration/admissionwebhook/webhook_patch_admission_webhook_configuration_withoutContext.go +++ b/test/integration/webhook/webhook_patch_admission_webhook_configuration_withoutContext.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package webhook import ( "context" diff --git a/test/integration/admissionwebhook/webhook_patch_secret_withoutContext.go b/test/integration/webhook/webhook_patch_secret_withoutContext.go similarity index 98% rename from test/integration/admissionwebhook/webhook_patch_secret_withoutContext.go rename to test/integration/webhook/webhook_patch_secret_withoutContext.go index 985bdcb5a..6f9ac8494 100644 --- a/test/integration/admissionwebhook/webhook_patch_secret_withoutContext.go +++ b/test/integration/webhook/webhook_patch_secret_withoutContext.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package admissionwebhook +package webhook import ( "context" diff --git a/test/internal/integration/apim/apim.go b/test/internal/integration/apim/apim.go index 5c84daeaf..6dfddd2cb 100644 --- a/test/internal/integration/apim/apim.go +++ b/test/internal/integration/apim/apim.go @@ -41,7 +41,7 @@ func NewClient(ctx context.Context) *APIM { Build(). Context - apim, err := apim.FromContext(ctx, context.Spec.Context) + apim, err := apim.FromContext(ctx, context, context.GetNamespace()) Expect(err).ToNot(HaveOccurred()) subscriptions := service.NewSubscriptions(apim.APIs.Client) diff --git a/test/internal/integration/assert/assert.go b/test/internal/integration/assert/assert.go index 43dee14eb..1ee7e4704 100644 --- a/test/internal/integration/assert/assert.go +++ b/test/internal/integration/assert/assert.go @@ -19,6 +19,7 @@ import ( "fmt" "net/http" "reflect" + "slices" "strings" "github.com/gravitee-io/gravitee-kubernetes-operator/api/v1alpha1" @@ -97,6 +98,15 @@ func Equals(field string, expected, given any) error { return nil } +func SliceEqualsSorted[S ~[]E, E any](field string, expected S, given S, comp func(a, b E) int) error { + ecp, gcp := make([]E, len(expected)), make([]E, len(given)) + copy(ecp, given) + slices.SortFunc(ecp, comp) + copy(gcp, expected) + slices.SortFunc(gcp, comp) + return Equals(field, ecp, gcp) +} + func NotEmptySlice[T any](field string, value []T) error { if len(value) == 0 { return fmt.Errorf("expected %#v not to be empty", field) @@ -118,6 +128,13 @@ func NotEmptyString(field string, value string) error { return nil } +func Nil(field string, value any) error { + if value != nil && !reflect.ValueOf(value).IsNil() { + return fmt.Errorf("expected %s to be nil", field) + } + return nil +} + func NotNil(field string, value any) error { if value == nil || reflect.ValueOf(value).IsNil() { return fmt.Errorf("expected %s not to be nil", field) diff --git a/test/internal/integration/constants/constants.go b/test/internal/integration/constants/constants.go index b17328223..d393f041e 100644 --- a/test/internal/integration/constants/constants.go +++ b/test/internal/integration/constants/constants.go @@ -25,7 +25,7 @@ import ( const ( Namespace = "default" - ConsistentTimeout = time.Second * 3 + ConsistentTimeout = time.Second * 2 EventualTimeout = time.Second * 30 Interval = time.Millisecond * 250 @@ -46,7 +46,7 @@ const ( ApiWithRateLimit = SamplesPath + "/apim/api_definition/v2/api-with-rate-limit.yml" ApiWithStateStopped = SamplesPath + "/apim/api_definition/v2/api-with-state-stopped.yml" ApiWithSyncFromAPIM = SamplesPath + "/apim/api_definition/v2/api-with-sync-from-apim.yml" - ApiWithIds = SamplesPath + "/apim/api_definition/v2/api-with-ids.yml" + ApiWithIDs = SamplesPath + "/apim/api_definition/v2/api-with-ids.yml" ApiWithDisabledPolicy = SamplesPath + "/apim/api_definition/v2/api-with-disabled-policy.yml" ApiWithTemplatingFile = SamplesPath + "/apim/api_definition/v2/api-with-templating.yml" ApiWithTemplatingSecretFile = SamplesPath + "/apim/api_definition/v2/api-with-templating-secret.yml" diff --git a/test/internal/integration/fixture/build.go b/test/internal/integration/fixture/build.go index 46e77fffc..14dd18694 100644 --- a/test/internal/integration/fixture/build.go +++ b/test/internal/integration/fixture/build.go @@ -206,7 +206,7 @@ func randomizeIngressRules(ing *netV1.Ingress, suffix string) { } } -func isTemplate(api custom.ApiDefinition) bool { +func isTemplate(api custom.ApiDefinitionResource) bool { return api.GetAnnotations()[keys.IngressTemplateAnnotation] == env.TrueString }