diff --git a/common-go-libs/PROJECT b/common-go-libs/PROJECT index 89fbcd315e..1bd4bc9e68 100644 --- a/common-go-libs/PROJECT +++ b/common-go-libs/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: wso2.com layout: - go.kubebuilder.io/v3 @@ -160,4 +164,16 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + domain: wso2.com + group: dp + kind: API + path: github.com/wso2/apk/common-go-libs/apis/dp/v1beta1 + version: v1beta1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/common-go-libs/apis/dp/v1alpha1/api_conversion.go b/common-go-libs/apis/dp/v1alpha1/api_conversion.go index 2aaf06c912..e6ffe0296d 100644 --- a/common-go-libs/apis/dp/v1alpha1/api_conversion.go +++ b/common-go-libs/apis/dp/v1alpha1/api_conversion.go @@ -18,15 +18,15 @@ package v1alpha1 import ( - "github.com/wso2/apk/common-go-libs/apis/dp/v1alpha2" + "github.com/wso2/apk/common-go-libs/apis/dp/v1beta1" "sigs.k8s.io/controller-runtime/pkg/conversion" ) -// ConvertTo converts this API CR to the Hub version (v1alpha2). -// src is v1alpha1.API and dst is v1alpha2.API. +// ConvertTo converts this API CR to the Hub version (v1beta1). +// src is v1alpha1.API and dst is v1beta1.API. func (src *API) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1alpha2.API) + dst := dstRaw.(*v1beta1.API) dst.ObjectMeta = src.ObjectMeta // Spec @@ -41,40 +41,40 @@ func (src *API) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.SystemAPI = src.Spec.SystemAPI if src.Spec.Production != nil { - dst.Spec.Production = []v1alpha2.EnvConfig{} + dst.Spec.Production = []v1beta1.EnvConfig{} for _, productionRef := range src.Spec.Production { - dst.Spec.Production = append(dst.Spec.Production, v1alpha2.EnvConfig{ + dst.Spec.Production = append(dst.Spec.Production, v1beta1.EnvConfig{ RouteRefs: productionRef.HTTPRouteRefs, }) } } if src.Spec.Sandbox != nil { - dst.Spec.Sandbox = []v1alpha2.EnvConfig{} + dst.Spec.Sandbox = []v1beta1.EnvConfig{} for _, sandboxRef := range src.Spec.Sandbox { - dst.Spec.Sandbox = append(dst.Spec.Sandbox, v1alpha2.EnvConfig{ + dst.Spec.Sandbox = append(dst.Spec.Sandbox, v1beta1.EnvConfig{ RouteRefs: sandboxRef.HTTPRouteRefs, }) } } // Convert []Property to []v1alpha2.Property - var properties []v1alpha2.Property + var properties []v1beta1.Property for _, p := range src.Spec.APIProperties { - properties = append(properties, v1alpha2.Property(p)) + properties = append(properties, v1beta1.Property(p)) } dst.Spec.APIProperties = properties // Status - dst.Status.DeploymentStatus = v1alpha2.DeploymentStatus(src.Status.DeploymentStatus) + dst.Status.DeploymentStatus = v1beta1.DeploymentStatus(src.Status.DeploymentStatus) return nil } -// ConvertFrom converts from the Hub version (v1alpha2) to this version. -// src is v1alpha1.API and dst is v1alpha2.API. +// ConvertFrom converts from the Hub version (v1beta1) to this version. +// src is v1alpha1.API and dst is v1beta1.API. func (src *API) ConvertFrom(srcRaw conversion.Hub) error { - dst := srcRaw.(*v1alpha2.API) + dst := srcRaw.(*v1beta1.API) src.ObjectMeta = dst.ObjectMeta // Spec diff --git a/common-go-libs/apis/dp/v1alpha2/api_conversion.go b/common-go-libs/apis/dp/v1alpha2/api_conversion.go index 5a1953e138..5fd93d09b4 100644 --- a/common-go-libs/apis/dp/v1alpha2/api_conversion.go +++ b/common-go-libs/apis/dp/v1alpha2/api_conversion.go @@ -17,5 +17,104 @@ package v1alpha2 -// Hub marks this type as a conversion hub. -func (*API) Hub() {} +import ( + "github.com/wso2/apk/common-go-libs/apis/dp/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertTo converts this API CR to the Hub version (v1beta1). +// src is v1alpha2.API and dst is v1beta1.API. +func (src *API) ConvertTo(dstRaw conversion.Hub) error { + + dst := dstRaw.(*v1beta1.API) + dst.ObjectMeta = src.ObjectMeta + + // Spec + dst.Spec.APIName = src.Spec.APIName + dst.Spec.APIVersion = src.Spec.APIVersion + dst.Spec.IsDefaultVersion = src.Spec.IsDefaultVersion + dst.Spec.DefinitionFileRef = src.Spec.DefinitionFileRef + dst.Spec.DefinitionPath = src.Spec.DefinitionPath + dst.Spec.APIType = src.Spec.APIType + dst.Spec.BasePath = src.Spec.BasePath + dst.Spec.Organization = src.Spec.Organization + dst.Spec.SystemAPI = src.Spec.SystemAPI + + if src.Spec.Production != nil { + dst.Spec.Production = []v1beta1.EnvConfig{} + for _, productionRef := range src.Spec.Production { + dst.Spec.Production = append(dst.Spec.Production, v1beta1.EnvConfig{ + RouteRefs: productionRef.RouteRefs, + }) + } + } + if src.Spec.Sandbox != nil { + dst.Spec.Sandbox = []v1beta1.EnvConfig{} + for _, sandboxRef := range src.Spec.Sandbox { + dst.Spec.Sandbox = append(dst.Spec.Sandbox, v1beta1.EnvConfig{ + RouteRefs: sandboxRef.RouteRefs, + }) + } + } + + // Convert []Property to []v1alpha2.Property + var properties []v1beta1.Property + for _, p := range src.Spec.APIProperties { + properties = append(properties, v1beta1.Property(p)) + } + dst.Spec.APIProperties = properties + + // Status + dst.Status.DeploymentStatus = v1beta1.DeploymentStatus(src.Status.DeploymentStatus) + + return nil +} + +// ConvertFrom converts from the Hub version (v1beta1) to this version. +// src is v1alpha2.API and dst is v1beta1.API. +func (src *API) ConvertFrom(srcRaw conversion.Hub) error { + + dst := srcRaw.(*v1beta1.API) + src.ObjectMeta = dst.ObjectMeta + + // Spec + src.Spec.APIName = dst.Spec.APIName + src.Spec.APIVersion = dst.Spec.APIVersion + src.Spec.IsDefaultVersion = dst.Spec.IsDefaultVersion + src.Spec.DefinitionFileRef = dst.Spec.DefinitionFileRef + src.Spec.DefinitionPath = dst.Spec.DefinitionPath + src.Spec.APIType = dst.Spec.APIType + src.Spec.BasePath = dst.Spec.BasePath + src.Spec.Organization = dst.Spec.Organization + src.Spec.SystemAPI = dst.Spec.SystemAPI + + if dst.Spec.Production != nil { + src.Spec.Production = []EnvConfig{} + for _, productionRef := range dst.Spec.Production { + src.Spec.Production = append(src.Spec.Production, EnvConfig{ + RouteRefs: productionRef.RouteRefs, + }) + } + } + + if dst.Spec.Sandbox != nil { + src.Spec.Sandbox = []EnvConfig{} + for _, sandboxRef := range dst.Spec.Sandbox { + src.Spec.Sandbox = append(src.Spec.Sandbox, EnvConfig{ + RouteRefs: sandboxRef.RouteRefs, + }) + } + } + + // Convert []Property to []v1alpha1.Property + var properties []Property + for _, p := range dst.Spec.APIProperties { + properties = append(properties, Property(p)) + } + src.Spec.APIProperties = properties + + // Status + src.Status.DeploymentStatus = DeploymentStatus(dst.Status.DeploymentStatus) + + return nil +} diff --git a/common-go-libs/apis/dp/v1alpha2/api_types.go b/common-go-libs/apis/dp/v1alpha2/api_types.go index 4579e5e0b9..f5b08467bb 100644 --- a/common-go-libs/apis/dp/v1alpha2/api_types.go +++ b/common-go-libs/apis/dp/v1alpha2/api_types.go @@ -172,7 +172,6 @@ type DeploymentStatus struct { // +genclient //+kubebuilder:object:root=true //+kubebuilder:subresource:status -//+kubebuilder:storageversion //+kubebuilder:printcolumn:name="API Name",type="string",JSONPath=".spec.apiName" //+kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.apiVersion" //+kubebuilder:printcolumn:name="BasePath",type="string",JSONPath=".spec.basePath" diff --git a/common-go-libs/apis/dp/v1alpha2/api_webhook.go b/common-go-libs/apis/dp/v1alpha2/api_webhook.go index 2e3348be35..7f62170633 100644 --- a/common-go-libs/apis/dp/v1alpha2/api_webhook.go +++ b/common-go-libs/apis/dp/v1alpha2/api_webhook.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "io" + "regexp" "strings" gqlparser "github.com/vektah/gqlparser" @@ -103,6 +104,8 @@ func (r *API) validateAPI() error { if r.Spec.BasePath == "" { allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("basePath"), "API basePath is required")) + } else if errMsg := validateAPIBasePathRegex(r.Spec.BasePath, r.Spec.APIType); errMsg != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("basePath"), r.Spec.BasePath, errMsg)) } else if errMsg := validateAPIBasePathFormat(r.Spec.BasePath, r.Spec.APIVersion); errMsg != "" { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("basePath"), r.Spec.BasePath, errMsg)) } else if err := r.validateAPIBasePathExistsAndDefaultVersion(); err != nil { @@ -159,6 +162,23 @@ func (r *API) validateAPI() error { return nil } +func validateAPIBasePathRegex(basePath, apiType string) string { + var pattern string + if apiType == "GRPC" { + pattern = `^[/][a-zA-Z][a-zA-Z0-9_.]*$` + } else { + pattern = `^[/][a-zA-Z0-9~/_.-]*$` + } + re, err := regexp.Compile(pattern) + if err != nil { + return "Failed to compile basePath regex pattern" + } + if !re.MatchString(basePath) { + return "API basePath is not in a valid format for the specified API type" + } + return "" +} + func isEmptyStringsInArray(strings []string) bool { for _, str := range strings { if str == "" { diff --git a/common-go-libs/apis/dp/v1beta1/api_conversion.go b/common-go-libs/apis/dp/v1beta1/api_conversion.go new file mode 100644 index 0000000000..5564f83e08 --- /dev/null +++ b/common-go-libs/apis/dp/v1beta1/api_conversion.go @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 v1beta1 + +// Hub marks this type as a conversion hub. +func (*API) Hub() {} diff --git a/common-go-libs/apis/dp/v1beta1/api_types.go b/common-go-libs/apis/dp/v1beta1/api_types.go new file mode 100644 index 0000000000..96e929bfe8 --- /dev/null +++ b/common-go-libs/apis/dp/v1beta1/api_types.go @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// APISpec defines the desired state of API +type APISpec struct { + + // APIName is the unique name of the API + //can be used to uniquely identify an API. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=60 + // +kubebuilder:validation:Pattern="^[^~!@#;:%^*()+={}|\\<>\"'',&$\\[\\]\\/]*$" + APIName string `json:"apiName"` + + // APIVersion is the version number of the API. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=30 + // +kubebuilder:validation:Pattern="^[^~!@#;:%^*()+={}|\\<>\"'',&/$\\[\\]\\s+\\/]+$" + APIVersion string `json:"apiVersion"` + + // IsDefaultVersion indicates whether this API version should be used as a default API + // + // +optional + IsDefaultVersion bool `json:"isDefaultVersion"` + + // DefinitionFileRef contains the + // definition of the API in a ConfigMap. + // + // +optional + DefinitionFileRef string `json:"definitionFileRef"` + + // DefinitionPath contains the path to expose the API definition. + // + // +kubebuilder:default:=/api-definition + // +kubebuilder:validation:MinLength=1 + DefinitionPath string `json:"definitionPath"` + + // Production contains a list of references to HttpRoutes + // of type HttpRoute. + // xref: https://github.com/kubernetes-sigs/gateway-api/blob/main/apis/v1beta1/httproute_types.go + // + // + // +optional + // +nullable + // +kubebuilder:validation:MaxItems=1 + Production []EnvConfig `json:"production"` + + // Sandbox contains a list of references to HttpRoutes + // of type HttpRoute. + // xref: https://github.com/kubernetes-sigs/gateway-api/blob/main/apis/v1beta1/httproute_types.go + // + // + // +optional + // +nullable + // +kubebuilder:validation:MaxItems=1 + Sandbox []EnvConfig `json:"sandbox"` + + // APIType denotes the type of the API. + // Possible values could be REST, GraphQL, gRPC + // + // +kubebuilder:validation:Enum=REST;GraphQL;gRPC + APIType string `json:"apiType"` + + // BasePath denotes the basepath of the API. + // e.g: /pet-store-api/1.0.6 + // + // +kubectl:validation:MaxLength=232 + // +kubebuilder:validation:Pattern=^[/][a-zA-Z0-9~/_.-]*$ + BasePath string `json:"basePath"` + + // Organization denotes the organization. + // related to the API + // + // +optional + Organization string `json:"organization"` + + // SystemAPI denotes if it is an internal system API. + // + // +optional + SystemAPI bool `json:"systemAPI"` + + // APIProperties denotes the custom properties of the API. + // + // +optional + // +nullable + APIProperties []Property `json:"apiProperties,omitempty"` + + // Environment denotes the environment of the API. + // + // +optional + // +nullable + Environment string `json:"environment,omitempty"` +} + +// Property holds key value pair of APIProperties +type Property struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} + +// EnvConfig contains the environment specific configuration +type EnvConfig struct { + // RouteRefs denotes the environment of the API. + RouteRefs []string `json:"routeRefs"` +} + +// APIStatus defines the observed state of API +type APIStatus struct { + // DeploymentStatus denotes the deployment status of the API + // + // +optional + DeploymentStatus DeploymentStatus `json:"deploymentStatus"` +} + +// DeploymentStatus contains the status of the API deployment +type DeploymentStatus struct { + + // Status denotes the state of the API in its lifecycle. + // Possible values could be Accepted, Invalid, Deploy etc. + // + // + Status string `json:"status"` + + // Message represents a user friendly message that explains the + // current state of the API. + // + // + // +optional + Message string `json:"message"` + + // Accepted represents whether the API is accepted or not. + // + // + Accepted bool `json:"accepted"` + + // TransitionTime represents the last known transition timestamp. + // + // + TransitionTime *metav1.Time `json:"transitionTime"` + + // Events contains a list of events related to the API. + // + // + // +optional + Events []string `json:"events,omitempty"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="API Name",type="string",JSONPath=".spec.apiName" +// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.apiVersion" +// +kubebuilder:printcolumn:name="BasePath",type="string",JSONPath=".spec.basePath" +// +kubebuilder:printcolumn:name="Organization",type="string",JSONPath=".spec.organization" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// API is the Schema for the apis API +type API struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec APISpec `json:"spec,omitempty"` + Status APIStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// APIList contains a list of API +type APIList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []API `json:"items"` +} + +func init() { + SchemeBuilder.Register(&API{}, &APIList{}) +} diff --git a/common-go-libs/apis/dp/v1beta1/api_webhook.go b/common-go-libs/apis/dp/v1beta1/api_webhook.go new file mode 100644 index 0000000000..0303745046 --- /dev/null +++ b/common-go-libs/apis/dp/v1beta1/api_webhook.go @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 v1beta1 + +import ( + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "strings" + + "github.com/sirupsen/logrus" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" + "github.com/wso2/apk/adapter/pkg/logging" + config "github.com/wso2/apk/common-go-libs/configs" + "github.com/wso2/apk/common-go-libs/loggers" + "github.com/wso2/apk/common-go-libs/utils" + "golang.org/x/exp/slices" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var apilog = logf.Log.WithName("api-resource") +var c client.Client + +// SetupWebhookWithManager creates a new webhook builder for API +func (r *API) SetupWebhookWithManager(mgr ctrl.Manager) error { + c = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-dp-wso2-com-v1beta1-api,mutating=true,failurePolicy=fail,sideEffects=None,groups=dp.wso2.com,resources=apis,verbs=create;update,versions=v1beta1,name=mapi.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &API{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *API) Default() { + apilog.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-dp-wso2-com-v1beta1-api,mutating=false,failurePolicy=fail,sideEffects=None,groups=dp.wso2.com,resources=apis,verbs=create;update,versions=v1beta1,name=vapi.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &API{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *API) ValidateCreate() (admission.Warnings, error) { + return nil, r.validateAPI() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *API) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + return nil, r.validateAPI() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *API) ValidateDelete() (admission.Warnings, error) { + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} + +// validateAPI validate api crd fields +func (r *API) validateAPI() error { + var allErrs field.ErrorList + conf := config.ReadConfigs() + namespaces := conf.CommonController.Operator.Namespaces + if len(namespaces) > 0 { + if !slices.Contains(namespaces, r.Namespace) { + loggers.LoggerAPK.Debugf("API validation Skipped for namespace: %v", r.Namespace) + return nil + } + } + + if r.Spec.BasePath == "" { + allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("basePath"), "API basePath is required")) + } else if errMsg := validateAPIBasePathFormat(r.Spec.BasePath, r.Spec.APIVersion); errMsg != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("basePath"), r.Spec.BasePath, errMsg)) + } else if err := r.validateAPIBasePathExistsAndDefaultVersion(); err != nil { + allErrs = append(allErrs, err) + } + + if r.Spec.DefinitionFileRef != "" { + if schemaString, errMsg := validateGzip(r.Spec.DefinitionFileRef, r.Namespace); errMsg != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("definitionFileRef"), r.Spec.DefinitionFileRef, errMsg)) + } else if schemaString != "" && r.Spec.APIType == "GraphQL" { + if errMsg := validateSDL(schemaString); errMsg != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("definitionFileRef"), r.Spec.DefinitionFileRef, errMsg)) + } + } + } else if r.Spec.APIType == "GraphQL" { + allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("definitionFileRef"), "GraphQL API definitionFileRef is required")) + } + + // Organization value should not be empty as it required when applying ratelimit policy + if r.Spec.Organization == "" { + allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("organization"), "Organization can not be empty")) + } + + if !(len(r.Spec.Production) > 0 && r.Spec.Production[0].RouteRefs != nil && len(r.Spec.Production[0].RouteRefs) > 0) && + !(len(r.Spec.Sandbox) > 0 && r.Spec.Sandbox[0].RouteRefs != nil && len(r.Spec.Sandbox[0].RouteRefs) > 0) { + allErrs = append(allErrs, field.Required(field.NewPath("spec"), + "both API production and sandbox endpoint references cannot be empty")) + } + + var prodHTTPRoute, sandHTTPRoute []string + if len(r.Spec.Production) > 0 { + prodHTTPRoute = r.Spec.Production[0].RouteRefs + } + if len(r.Spec.Sandbox) > 0 { + sandHTTPRoute = r.Spec.Sandbox[0].RouteRefs + } + + if isEmptyStringsInArray(prodHTTPRoute) { + allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("production").Child("httpRouteRefs"), + "API production endpoint reference cannot be empty")) + } + + if isEmptyStringsInArray(sandHTTPRoute) { + allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("sandbox").Child("httpRouteRefs"), + "API sandbox endpoint reference cannot be empty")) + } + + if len(allErrs) > 0 { + return apierrors.NewInvalid( + schema.GroupKind{Group: "dp.wso2.com", Kind: "API"}, + r.Name, allErrs) + } + + return nil +} + +func isEmptyStringsInArray(strings []string) bool { + for _, str := range strings { + if str == "" { + return true + } + } + return false +} + +func (r *API) validateAPIBasePathExistsAndDefaultVersion() *field.Error { + + apiList, err := retrieveAPIList() + if err != nil { + return field.InternalError(field.NewPath("spec").Child("basePath"), + errors.New("unable to list APIs for API basePath validation")) + + } + currentAPIBasePathWithoutVersion := getBasePathWithoutVersion(r.Spec.BasePath) + incomingAPIEnvironment := utils.GetEnvironment(r.Spec.Environment) + for _, api := range apiList { + if (types.NamespacedName{Namespace: r.Namespace, Name: r.Name} != + types.NamespacedName{Namespace: api.Namespace, Name: api.Name}) { + + existingAPIEnvironment := utils.GetEnvironment(api.Spec.Environment) + if api.Spec.Organization == r.Spec.Organization && api.Spec.BasePath == r.Spec.BasePath && + incomingAPIEnvironment == existingAPIEnvironment { + return &field.Error{ + Type: field.ErrorTypeDuplicate, + Field: field.NewPath("spec").Child("basePath").String(), + BadValue: r.Spec.BasePath, + Detail: "an API has been already created for the basePath"} + } + if r.Spec.IsDefaultVersion { + targetAPIBasePathWithoutVersion := getBasePathWithoutVersion(api.Spec.BasePath) + targetAPIBasePathWithVersion := api.Spec.BasePath + if api.Spec.IsDefaultVersion { + if targetAPIBasePathWithoutVersion == currentAPIBasePathWithoutVersion { + return &field.Error{ + Type: field.ErrorTypeForbidden, + Field: field.NewPath("spec").Child("isDefaultVersion").String(), + BadValue: r.Spec.BasePath, + Detail: "this API already has a default version"} + } + + } + if targetAPIBasePathWithVersion == currentAPIBasePathWithoutVersion { + return &field.Error{ + Type: field.ErrorTypeForbidden, + Field: field.NewPath("spec").Child("isDefaultVersion").String(), + BadValue: r.Spec.BasePath, + Detail: fmt.Sprintf("api: %s's basePath path is colliding with default path", r.Name)} + } + } + } + } + return nil +} + +func retrieveAPIList() ([]API, error) { + ctx := context.Background() + conf := config.ReadConfigs() + namespaces := conf.CommonController.Operator.Namespaces + var apis []API + if namespaces == nil { + apiList := &APIList{} + if err := c.List(ctx, apiList, &client.ListOptions{}); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2605, logging.CRITICAL, "Unable to list APIs: %v", err.Error())) + return nil, err + } + apis = make([]API, len(apiList.Items)) + copy(apis[:], apiList.Items[:]) + } else { + for _, namespace := range namespaces { + apiList := &APIList{} + if err := c.List(ctx, apiList, &client.ListOptions{Namespace: namespace}); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2605, logging.CRITICAL, "Unable to list APIs: %v", err.Error())) + return nil, err + } + apis = append(apis, apiList.Items...) + } + } + return apis, nil +} + +func validateAPIBasePathFormat(basePath string, apiVersion string) string { + if !strings.HasSuffix("/"+basePath, apiVersion) { + return "API basePath value should contain the /{APIVersion} at end." + } + return "" +} + +// getBasePathWithoutVersion returns the basePath without version +func getBasePathWithoutVersion(basePath string) string { + lastIndex := strings.LastIndex(basePath, "/") + if lastIndex != -1 { + return basePath[:lastIndex] + } + return basePath +} + +func validateGzip(name, namespace string) (string, string) { + configMap := &corev1.ConfigMap{} + var err error + + if err = c.Get(context.Background(), types.NamespacedName{Name: string(name), Namespace: namespace}, configMap); err == nil { + var apiDef []byte + for _, val := range configMap.BinaryData { + // config map data key is "swagger.yaml" + apiDef = []byte(val) + } + // unzip gzip bytes + var schemaString string + if schemaString, err = unzip(apiDef); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2600, logging.MINOR, "Error while unzipping gzip bytes: %v", err)) + return "", "invalid gzipped content" + } + return schemaString, "" + } + + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2600, logging.MINOR, "ConfigMap for sdl not found: %v", err)) + + return "", "" +} + +// unzip gzip bytes +func unzip(compressedData []byte) (string, error) { + reader, err := gzip.NewReader(bytes.NewBuffer(compressedData)) + if err != nil { + logrus.Info(err) + return "", fmt.Errorf("error creating gzip reader: %v", err) + } + defer reader.Close() + + // Read the decompressed data + schemaString, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("error reading decompressed data of the apiDefinition: %v", err) + } + return string(schemaString), nil +} + +func validateSDL(sdl string) string { + _, err := gqlparser.LoadSchema(&ast.Source{Input: sdl}) + if err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2600, logging.MINOR, "Error while parsing the GraphQL SDL: %v", err)) + return "error while parsing the GraphQL SDL" + } + return "" +} diff --git a/common-go-libs/apis/dp/v1beta1/groupversion_info.go b/common-go-libs/apis/dp/v1beta1/groupversion_info.go new file mode 100644 index 0000000000..9f7ad3d004 --- /dev/null +++ b/common-go-libs/apis/dp/v1beta1/groupversion_info.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 v1beta1 contains API Schema definitions for the dp v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=dp.wso2.com +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "dp.wso2.com", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/common-go-libs/apis/dp/v1beta1/webhook_suite_test.go b/common-go-libs/apis/dp/v1beta1/webhook_suite_test.go new file mode 100644 index 0000000000..4c43950443 --- /dev/null +++ b/common-go-libs/apis/dp/v1beta1/webhook_suite_test.go @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 v1beta1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + //+kubebuilder:scaffold:imports + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + LeaderElection: false, + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&API{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/common-go-libs/apis/dp/v1beta1/zz_generated.deepcopy.go b/common-go-libs/apis/dp/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..79cffd6dbb --- /dev/null +++ b/common-go-libs/apis/dp/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,195 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* + * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *API) DeepCopyInto(out *API) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new API. +func (in *API) DeepCopy() *API { + if in == nil { + return nil + } + out := new(API) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *API) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIList) DeepCopyInto(out *APIList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]API, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIList. +func (in *APIList) DeepCopy() *APIList { + if in == nil { + return nil + } + out := new(APIList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APISpec) DeepCopyInto(out *APISpec) { + *out = *in + if in.Production != nil { + in, out := &in.Production, &out.Production + *out = make([]EnvConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Sandbox != nil { + in, out := &in.Sandbox, &out.Sandbox + *out = make([]EnvConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.APIProperties != nil { + in, out := &in.APIProperties, &out.APIProperties + *out = make([]Property, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APISpec. +func (in *APISpec) DeepCopy() *APISpec { + if in == nil { + return nil + } + out := new(APISpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIStatus) DeepCopyInto(out *APIStatus) { + *out = *in + in.DeploymentStatus.DeepCopyInto(&out.DeploymentStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIStatus. +func (in *APIStatus) DeepCopy() *APIStatus { + if in == nil { + return nil + } + out := new(APIStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { + *out = *in + if in.TransitionTime != nil { + in, out := &in.TransitionTime, &out.TransitionTime + *out = (*in).DeepCopy() + } + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatus. +func (in *DeploymentStatus) DeepCopy() *DeploymentStatus { + if in == nil { + return nil + } + out := new(DeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvConfig) DeepCopyInto(out *EnvConfig) { + *out = *in + if in.RouteRefs != nil { + in, out := &in.RouteRefs, &out.RouteRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvConfig. +func (in *EnvConfig) DeepCopy() *EnvConfig { + if in == nil { + return nil + } + out := new(EnvConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Property) DeepCopyInto(out *Property) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Property. +func (in *Property) DeepCopy() *Property { + if in == nil { + return nil + } + out := new(Property) + in.DeepCopyInto(out) + return out +} diff --git a/common-go-libs/config/certmanager/certificate.yaml b/common-go-libs/config/certmanager/certificate.yaml new file mode 100644 index 0000000000..4321a02d48 --- /dev/null +++ b/common-go-libs/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: issuer + app.kubernetes.io/instance: selfsigned-issuer + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/common-go-libs/config/certmanager/kustomization.yaml b/common-go-libs/config/certmanager/kustomization.yaml new file mode 100644 index 0000000000..bebea5a595 --- /dev/null +++ b/common-go-libs/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/common-go-libs/config/certmanager/kustomizeconfig.yaml b/common-go-libs/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 0000000000..e631f77736 --- /dev/null +++ b/common-go-libs/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,16 @@ +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml b/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml index 7846de3caa..b3dee8a220 100644 --- a/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml +++ b/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml @@ -360,6 +360,184 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.apiName + name: API Name + type: string + - jsonPath: .spec.apiVersion + name: Version + type: string + - jsonPath: .spec.basePath + name: BasePath + type: string + - jsonPath: .spec.organization + name: Organization + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: API is the Schema for the apis API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: APISpec defines the desired state of API + properties: + apiName: + description: APIName is the unique name of the API can be used to + uniquely identify an API. + maxLength: 60 + minLength: 1 + pattern: ^[^~!@#;:%^*()+={}|\<>"'',&$\[\]\/]*$ + type: string + apiProperties: + description: APIProperties denotes the custom properties of the API. + items: + description: Property holds key value pair of APIProperties + properties: + name: + type: string + value: + type: string + type: object + nullable: true + type: array + apiType: + description: APIType denotes the type of the API. Possible values + could be REST, GraphQL, gRPC + enum: + - REST + - GraphQL + - gRPC + type: string + apiVersion: + description: APIVersion is the version number of the API. + maxLength: 30 + minLength: 1 + pattern: ^[^~!@#;:%^*()+={}|\<>"'',&/$\[\]\s+\/]+$ + type: string + basePath: + description: 'BasePath denotes the basepath of the API. e.g: /pet-store-api/1.0.6' + pattern: ^[/][a-zA-Z0-9~/_.-]*$ + type: string + definitionFileRef: + description: DefinitionFileRef contains the definition of the API + in a ConfigMap. + type: string + definitionPath: + default: /api-definition + description: DefinitionPath contains the path to expose the API definition. + minLength: 1 + type: string + environment: + description: Environment denotes the environment of the API. + nullable: true + type: string + isDefaultVersion: + description: IsDefaultVersion indicates whether this API version should + be used as a default API + type: boolean + organization: + description: Organization denotes the organization. related to the + API + type: string + production: + description: 'Production contains a list of references to HttpRoutes + of type HttpRoute. xref: https://github.com/kubernetes-sigs/gateway-api/blob/main/apis/v1beta1/httproute_types.go' + items: + description: EnvConfig contains the environment specific configuration + properties: + routeRefs: + description: RouteRefs denotes the environment of the API. + items: + type: string + type: array + required: + - routeRefs + type: object + maxItems: 1 + nullable: true + type: array + sandbox: + description: 'Sandbox contains a list of references to HttpRoutes + of type HttpRoute. xref: https://github.com/kubernetes-sigs/gateway-api/blob/main/apis/v1beta1/httproute_types.go' + items: + description: EnvConfig contains the environment specific configuration + properties: + routeRefs: + description: RouteRefs denotes the environment of the API. + items: + type: string + type: array + required: + - routeRefs + type: object + maxItems: 1 + nullable: true + type: array + systemAPI: + description: SystemAPI denotes if it is an internal system API. + type: boolean + required: + - apiName + - apiType + - apiVersion + - basePath + - definitionPath + type: object + status: + description: APIStatus defines the observed state of API + properties: + deploymentStatus: + description: DeploymentStatus denotes the deployment status of the + API + properties: + accepted: + description: Accepted represents whether the API is accepted or + not. + type: boolean + events: + description: Events contains a list of events related to the API. + items: + type: string + type: array + message: + description: Message represents a user friendly message that explains + the current state of the API. + type: string + status: + description: Status denotes the state of the API in its lifecycle. + Possible values could be Accepted, Invalid, Deploy etc. + type: string + transitionTime: + description: TransitionTime represents the last known transition + timestamp. + format: date-time + type: string + required: + - accepted + - status + - transitionTime + type: object + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/common-go-libs/config/crd/kustomization.yaml b/common-go-libs/config/crd/kustomization.yaml new file mode 100644 index 0000000000..f5a7460e0d --- /dev/null +++ b/common-go-libs/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/dp.wso2.com_apis.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_apis.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_apis.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/common-go-libs/config/crd/kustomizeconfig.yaml b/common-go-libs/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000000..ec5c150a9d --- /dev/null +++ b/common-go-libs/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/common-go-libs/config/crd/patches/cainjection_in_dp_apis.yaml b/common-go-libs/config/crd/patches/cainjection_in_dp_apis.yaml new file mode 100644 index 0000000000..7f949667b8 --- /dev/null +++ b/common-go-libs/config/crd/patches/cainjection_in_dp_apis.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: apis.dp.wso2.com diff --git a/common-go-libs/config/crd/patches/webhook_in_dp_apis.yaml b/common-go-libs/config/crd/patches/webhook_in_dp_apis.yaml new file mode 100644 index 0000000000..ad0c609d56 --- /dev/null +++ b/common-go-libs/config/crd/patches/webhook_in_dp_apis.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: apis.dp.wso2.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/common-go-libs/config/default/manager_webhook_patch.yaml b/common-go-libs/config/default/manager_webhook_patch.yaml new file mode 100644 index 0000000000..738de350b7 --- /dev/null +++ b/common-go-libs/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/common-go-libs/config/default/webhookcainjection_patch.yaml b/common-go-libs/config/default/webhookcainjection_patch.yaml new file mode 100644 index 0000000000..76add1fe00 --- /dev/null +++ b/common-go-libs/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,29 @@ +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: mutatingwebhookconfiguration + app.kubernetes.io/instance: mutating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/common-go-libs/config/rbac/dp_api_editor_role.yaml b/common-go-libs/config/rbac/dp_api_editor_role.yaml new file mode 100644 index 0000000000..2de3070c5b --- /dev/null +++ b/common-go-libs/config/rbac/dp_api_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit apis. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: api-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: api-editor-role +rules: +- apiGroups: + - dp.wso2.com + resources: + - apis + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dp.wso2.com + resources: + - apis/status + verbs: + - get diff --git a/common-go-libs/config/rbac/dp_api_viewer_role.yaml b/common-go-libs/config/rbac/dp_api_viewer_role.yaml new file mode 100644 index 0000000000..75e4c488a0 --- /dev/null +++ b/common-go-libs/config/rbac/dp_api_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view apis. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: api-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: api-viewer-role +rules: +- apiGroups: + - dp.wso2.com + resources: + - apis + verbs: + - get + - list + - watch +- apiGroups: + - dp.wso2.com + resources: + - apis/status + verbs: + - get diff --git a/common-go-libs/config/samples/dp_v1beta1_api.yaml b/common-go-libs/config/samples/dp_v1beta1_api.yaml new file mode 100644 index 0000000000..c84dd75cdb --- /dev/null +++ b/common-go-libs/config/samples/dp_v1beta1_api.yaml @@ -0,0 +1,12 @@ +apiVersion: dp.wso2.com/v1beta1 +kind: API +metadata: + labels: + app.kubernetes.io/name: api + app.kubernetes.io/instance: api-sample + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: operator + name: api-sample +spec: + # TODO(user): Add fields here diff --git a/common-go-libs/config/webhook/kustomization.yaml b/common-go-libs/config/webhook/kustomization.yaml new file mode 100644 index 0000000000..9cf26134e4 --- /dev/null +++ b/common-go-libs/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/common-go-libs/config/webhook/kustomizeconfig.yaml b/common-go-libs/config/webhook/kustomizeconfig.yaml new file mode 100644 index 0000000000..25e21e3c96 --- /dev/null +++ b/common-go-libs/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,25 @@ +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/common-go-libs/config/webhook/manifests.yaml b/common-go-libs/config/webhook/manifests.yaml index 420c9ebd4a..1a2a720f27 100644 --- a/common-go-libs/config/webhook/manifests.yaml +++ b/common-go-libs/config/webhook/manifests.yaml @@ -4,6 +4,26 @@ kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-dp-wso2-com-v1beta1-api + failurePolicy: Fail + name: mapi.kb.io + rules: + - apiGroups: + - dp.wso2.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - apis + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -170,6 +190,26 @@ kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-dp-wso2-com-v1beta1-api + failurePolicy: Fail + name: vapi.kb.io + rules: + - apiGroups: + - dp.wso2.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - apis + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/common-go-libs/config/webhook/service.yaml b/common-go-libs/config/webhook/service.yaml new file mode 100644 index 0000000000..3d52bb199a --- /dev/null +++ b/common-go-libs/config/webhook/service.yaml @@ -0,0 +1,20 @@ + +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + app.kubernetes.io/instance: webhook-service + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: operator + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/common-go-libs/revive.toml b/common-go-libs/revive.toml index 46dfa7325e..2a89307bda 100644 --- a/common-go-libs/revive.toml +++ b/common-go-libs/revive.toml @@ -1,14 +1,14 @@ -ignoreGeneratedHeader = false -severity = "warning" confidence = 0.8 errorCode = 0 +ignoreGeneratedHeader = false +severity = "warning" warningCode = 0 [rule.blank-imports] [rule.context-as-argument] [rule.context-keys-type] [rule.dot-imports] -exclude = ["apis/dp/v1alpha1/webhook_suite_test.go", "apis/dp/v1alpha2/webhook_suite_test.go"] +exclude = ["apis/dp/v1alpha1/webhook_suite_test.go", "apis/dp/v1alpha2/webhook_suite_test.go", "apis/dp/v1beta1/webhook_suite_test.go"] [rule.error-return] [rule.error-strings] [rule.error-naming]