diff --git a/Makefile b/Makefile index 169dbe60d..bbabde52f 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,13 @@ else GOBIN=$(shell $(GO) env GOBIN) endif +# Set sed -i as it's different for mac vs gnu +ifeq ($(shell uname -s | tr A-Z a-z), darwin) + SED_INLINE ?= sed -i '' +else + SED_INLINE ?= sed -i +endif + MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) PROJECT_PATH := $(patsubst %/,%,$(dir $(MKFILE_PATH))) @@ -85,6 +92,7 @@ manager: generate fmt vet # Run against the configured Kubernetes cluster in ~/.kube/config run: export WATCH_NAMESPACE=$(LOCAL_RUN_NAMESPACE) run: export THREESCALE_DEBUG=1 +run: export ENABLE_WEBHOOKS=false run: generate fmt vet manifests $(GO) run ./main.go --zap-devel @@ -199,7 +207,7 @@ bundle-custom-updates: $(YQ) $(YQ) --inplace '.spec.displayName = "$(BUNDLE_PREFIX) 3scale operator"' $(PROJECT_PATH)/bundle/manifests/3scale-operator.clusterserviceversion.yaml $(YQ) --inplace '.spec.provider.name = "$(BUNDLE_PREFIX)"' $(PROJECT_PATH)/bundle/manifests/3scale-operator.clusterserviceversion.yaml $(YQ) --inplace '.annotations."operators.operatorframework.io.bundle.package.v1" = "$(BUNDLE_PREFIX)-3scale-operator"' $(PROJECT_PATH)/bundle/metadata/annotations.yaml - sed -E -i 's/(operators\.operatorframework\.io\.bundle\.package\.v1=).+/\1$(BUNDLE_PREFIX)-3scale-operator/' $(PROJECT_PATH)/bundle.Dockerfile + $(SED_INLINE) -E 's/(operators\.operatorframework\.io\.bundle\.package\.v1=).+/\1$(BUNDLE_PREFIX)-3scale-operator/' $(PROJECT_PATH)/bundle.Dockerfile @echo "Update operator image reference URL" $(YQ) --inplace '.metadata.annotations.containerImage = "$(IMG)"' $(PROJECT_PATH)/bundle/manifests/3scale-operator.clusterserviceversion.yaml $(YQ) --inplace '.spec.install.spec.deployments[0].spec.template.spec.containers[0].image = "$(IMG)"' $(PROJECT_PATH)/bundle/manifests/3scale-operator.clusterserviceversion.yaml diff --git a/PROJECT b/PROJECT index 749755166..c8acda042 100644 --- a/PROJECT +++ b/PROJECT @@ -43,6 +43,9 @@ resources: - group: capabilities kind: Application version: v1beta1 +- group: capabilities + kind: Product + version: v1beta2 version: 3-alpha plugins: go.sdk.operatorframework.io/v2-alpha: {} diff --git a/apis/capabilities/v1beta1/backend_types.go b/apis/capabilities/v1beta1/backend_types.go index 4821b7b4c..000e334d6 100644 --- a/apis/capabilities/v1beta1/backend_types.go +++ b/apis/capabilities/v1beta1/backend_types.go @@ -21,6 +21,7 @@ import ( "regexp" "strings" + "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" @@ -90,14 +91,14 @@ type BackendSpec struct { // system_name attr is unique for all metrics AND methods // In other words, if metric's system_name is A, there is no metric or method with system_name A. // +optional - Metrics map[string]MetricSpec `json:"metrics,omitempty"` + Metrics map[string]v1beta2.MetricSpec `json:"metrics,omitempty"` // Methods // Map: system_name -> MethodSpec // system_name attr is unique for all metrics AND methods // In other words, if metric's system_name is A, there is no metric or method with system_name A. // +optional - Methods map[string]MethodSpec `json:"methods,omitempty"` + Methods map[string]v1beta2.MethodSpec `json:"methods,omitempty"` // ProviderAccountRef references account provider credentials // +optional @@ -177,7 +178,7 @@ func (backend *Backend) SetDefaults(logger logr.Logger) bool { } if backend.Spec.Metrics == nil { - backend.Spec.Metrics = map[string]MetricSpec{} + backend.Spec.Metrics = map[string]v1beta2.MetricSpec{} updated = true } @@ -190,7 +191,7 @@ func (backend *Backend) SetDefaults(logger logr.Logger) bool { } if !hitsFound { logger.V(1).Info("Hits metric added") - backend.Spec.Metrics["hits"] = MetricSpec{ + backend.Spec.Metrics["hits"] = v1beta2.MetricSpec{ Name: "Hits", Unit: "hit", Description: "Number of API hits", diff --git a/apis/capabilities/v1beta1/product_conversion.go b/apis/capabilities/v1beta1/product_conversion.go new file mode 100644 index 000000000..b7a920bad --- /dev/null +++ b/apis/capabilities/v1beta1/product_conversion.go @@ -0,0 +1,406 @@ +package v1beta1 + +import ( + "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertTo converts this Product to the Hub version (v1beta2). +func (src *Product) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1beta2.Product) + + // Set the ObjectMeta + dst.ObjectMeta = src.ObjectMeta + + // Set the spec + dst.Spec.Name = src.Spec.Name + dst.Spec.SystemName = src.Spec.SystemName + dst.Spec.Description = src.Spec.Description + if src.Spec.Deployment != nil { + dst.Spec.Deployment = &v1beta2.ProductDeploymentSpec{} + if src.Spec.Deployment.ApicastHosted != nil && src.Spec.Deployment.ApicastHosted.Authentication != nil { + dst.Spec.Deployment.ApicastHosted = &v1beta2.ApicastHostedSpec{ + Authentication: &v1beta2.AuthenticationSpec{}, + } + if src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Key = src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Key + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.CredentialsLoc = src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.CredentialsLoc + if src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security = &v1beta2.SecuritySpec{ + HostHeader: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security.HostHeader, + SecretToken: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security.SecretToken, + } + } + if src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse = &v1beta2.GatewayResponseSpec{ + ErrorStatusAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusAuthFailed, + ErrorHeadersAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersAuthFailed, + ErrorAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorAuthFailed, + ErrorStatusAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusAuthMissing, + ErrorHeadersAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersAuthMissing, + ErrorAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorAuthMissing, + ErrorStatusNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusNoMatch, + ErrorHeadersNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersNoMatch, + ErrorNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorNoMatch, + ErrorStatusLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusLimitsExceeded, + ErrorHeadersLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersLimitsExceeded, + ErrorLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorLimitsExceeded, + } + } + + } + if src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication = &v1beta2.AppKeyAppIDAuthenticationSpec{ + AppID: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.AppID, + AppKey: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.AppKey, + CredentialsLoc: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.CredentialsLoc, + } + if src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security = &v1beta2.SecuritySpec{ + HostHeader: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security.HostHeader, + SecretToken: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security.SecretToken, + } + } + if src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse = &v1beta2.GatewayResponseSpec{ + ErrorStatusAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusAuthFailed, + ErrorHeadersAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersAuthFailed, + ErrorAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorAuthFailed, + ErrorStatusAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusAuthMissing, + ErrorHeadersAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersAuthMissing, + ErrorAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorAuthMissing, + ErrorStatusNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusNoMatch, + ErrorHeadersNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersNoMatch, + ErrorNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorNoMatch, + ErrorStatusLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusLimitsExceeded, + ErrorHeadersLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersLimitsExceeded, + ErrorLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorLimitsExceeded, + } + } + + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC = &v1beta2.OIDCSpec{ + IssuerType: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.IssuerType, + IssuerEndpoint: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.IssuerEndpoint, + JwtClaimWithClientID: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.JwtClaimWithClientID, + JwtClaimWithClientIDType: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.JwtClaimWithClientIDType, + CredentialsLoc: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.CredentialsLoc, + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow = &v1beta2.OIDCAuthenticationFlowSpec{ + StandardFlowEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.StandardFlowEnabled, + ImplicitFlowEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.ImplicitFlowEnabled, + ServiceAccountsEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.ServiceAccountsEnabled, + DirectAccessGrantsEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.DirectAccessGrantsEnabled, + } + + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security = &v1beta2.SecuritySpec{ + HostHeader: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security.HostHeader, + SecretToken: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security.SecretToken, + } + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse = &v1beta2.GatewayResponseSpec{ + ErrorStatusAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusAuthFailed, + ErrorHeadersAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersAuthFailed, + ErrorAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorAuthFailed, + ErrorStatusAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusAuthMissing, + ErrorHeadersAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersAuthMissing, + ErrorAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorAuthMissing, + ErrorStatusNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusNoMatch, + ErrorHeadersNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersNoMatch, + ErrorNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorNoMatch, + ErrorStatusLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusLimitsExceeded, + ErrorHeadersLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersLimitsExceeded, + ErrorLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorLimitsExceeded, + } + } + } + } + } + + dst.Spec.MappingRules = []v1beta2.MappingRuleSpec{} + for i := range src.Spec.MappingRules { + dst.Spec.MappingRules = append(dst.Spec.MappingRules, v1beta2.MappingRuleSpec{ + HTTPMethod: src.Spec.MappingRules[i].HTTPMethod, + Pattern: src.Spec.MappingRules[i].Pattern, + MetricMethodRef: src.Spec.MappingRules[i].MetricMethodRef, + Increment: src.Spec.MappingRules[i].Increment, + Last: src.Spec.MappingRules[i].Last, + }) + } + + dst.Spec.BackendUsages = map[string]v1beta2.BackendUsageSpec{} + for s := range src.Spec.BackendUsages { + dst.Spec.BackendUsages[s] = v1beta2.BackendUsageSpec{ + Path: src.Spec.BackendUsages[s].Path, + } + } + + dst.Spec.Metrics = map[string]v1beta2.MetricSpec{} + for s := range src.Spec.Metrics { + dst.Spec.Metrics[s] = v1beta2.MetricSpec{ + Name: src.Spec.Metrics[s].Name, + Unit: src.Spec.Metrics[s].Unit, + Description: src.Spec.Metrics[s].Description, + } + } + + dst.Spec.Methods = map[string]v1beta2.MethodSpec{} + for s := range src.Spec.Methods { + dst.Spec.Methods[s] = v1beta2.MethodSpec{ + Name: src.Spec.Methods[s].Name, + Description: src.Spec.Methods[s].Description, + } + } + + dst.Spec.ApplicationPlans = map[string]v1beta2.ApplicationPlanSpec{} + for s := range src.Spec.ApplicationPlans { + plan := v1beta2.ApplicationPlanSpec{ + Name: src.Spec.ApplicationPlans[s].Name, + AppsRequireApproval: src.Spec.ApplicationPlans[s].AppsRequireApproval, + TrialPeriod: src.Spec.ApplicationPlans[s].TrialPeriod, + SetupFee: src.Spec.ApplicationPlans[s].SetupFee, + CostMonth: src.Spec.ApplicationPlans[s].CostMonth, + PricingRules: nil, + Limits: nil, + Published: src.Spec.ApplicationPlans[s].Published, + } + + for i := range src.Spec.ApplicationPlans[s].PricingRules { + plan.PricingRules = append(plan.PricingRules, v1beta2.PricingRuleSpec{ + From: src.Spec.ApplicationPlans[s].PricingRules[i].From, + To: src.Spec.ApplicationPlans[s].PricingRules[i].To, + MetricMethodRef: v1beta2.MetricMethodRefSpec{ + SystemName: src.Spec.ApplicationPlans[s].PricingRules[i].MetricMethodRef.SystemName, + BackendSystemName: src.Spec.ApplicationPlans[s].PricingRules[i].MetricMethodRef.BackendSystemName, + }, + PricePerUnit: src.Spec.ApplicationPlans[s].PricingRules[i].PricePerUnit, + }) + } + + dst.Spec.ApplicationPlans[s] = plan + } + + dst.Spec.ProviderAccountRef = src.Spec.ProviderAccountRef + + for i := range src.Spec.Policies { + dst.Spec.Policies = append(dst.Spec.Policies, v1beta2.PolicyConfig{ + Name: src.Spec.Policies[i].Name, + Version: src.Spec.Policies[i].Version, + Configuration: v1beta2.Configuration{Value: src.Spec.Policies[i].Configuration}, + Enabled: src.Spec.Policies[i].Enabled, + }) + } + + // Set the status + dst.Status.ID = src.Status.ID + dst.Status.State = src.Status.State + dst.Status.ProviderAccountHost = src.Status.ProviderAccountHost + dst.Status.ObservedGeneration = src.Status.ObservedGeneration + dst.Status.Conditions = src.Status.Conditions + return nil +} + +// ConvertFrom converts from the Hub version (v1beta2) to this version. +func (dst *Product) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1beta2.Product) + + // Set the ObjectMeta + dst.ObjectMeta = src.ObjectMeta + + // Set the spec + dst.Spec.Name = src.Spec.Name + dst.Spec.SystemName = src.Spec.SystemName + dst.Spec.Description = src.Spec.Description + if src.Spec.Deployment != nil { + dst.Spec.Deployment = &ProductDeploymentSpec{} + if src.Spec.Deployment.ApicastHosted != nil && src.Spec.Deployment.ApicastHosted.Authentication != nil { + dst.Spec.Deployment.ApicastHosted = &ApicastHostedSpec{ + Authentication: &AuthenticationSpec{}, + } + if src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Key = src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Key + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.CredentialsLoc = src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.CredentialsLoc + if src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security = &SecuritySpec{ + HostHeader: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security.HostHeader, + SecretToken: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.Security.SecretToken, + } + } + if src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse = &GatewayResponseSpec{ + ErrorStatusAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusAuthFailed, + ErrorHeadersAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersAuthFailed, + ErrorAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorAuthFailed, + ErrorStatusAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusAuthMissing, + ErrorHeadersAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersAuthMissing, + ErrorAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorAuthMissing, + ErrorStatusNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusNoMatch, + ErrorHeadersNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersNoMatch, + ErrorNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorNoMatch, + ErrorStatusLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorStatusLimitsExceeded, + ErrorHeadersLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorHeadersLimitsExceeded, + ErrorLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.UserKeyAuthentication.GatewayResponse.ErrorLimitsExceeded, + } + } + + } + if src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication = &AppKeyAppIDAuthenticationSpec{ + AppID: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.AppID, + AppKey: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.AppKey, + CredentialsLoc: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.CredentialsLoc, + } + if src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security = &SecuritySpec{ + HostHeader: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security.HostHeader, + SecretToken: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.Security.SecretToken, + } + } + if src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse = &GatewayResponseSpec{ + ErrorStatusAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusAuthFailed, + ErrorHeadersAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersAuthFailed, + ErrorAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorAuthFailed, + ErrorStatusAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusAuthMissing, + ErrorHeadersAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersAuthMissing, + ErrorAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorAuthMissing, + ErrorStatusNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusNoMatch, + ErrorHeadersNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersNoMatch, + ErrorNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorNoMatch, + ErrorStatusLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorStatusLimitsExceeded, + ErrorHeadersLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorHeadersLimitsExceeded, + ErrorLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.AppKeyAppIDAuthentication.GatewayResponse.ErrorLimitsExceeded, + } + } + + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC = &OIDCSpec{ + IssuerType: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.IssuerType, + IssuerEndpoint: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.IssuerEndpoint, + JwtClaimWithClientID: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.JwtClaimWithClientID, + JwtClaimWithClientIDType: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.JwtClaimWithClientIDType, + CredentialsLoc: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.CredentialsLoc, + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow = &OIDCAuthenticationFlowSpec{ + StandardFlowEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.StandardFlowEnabled, + ImplicitFlowEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.ImplicitFlowEnabled, + ServiceAccountsEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.ServiceAccountsEnabled, + DirectAccessGrantsEnabled: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.AuthenticationFlow.DirectAccessGrantsEnabled, + } + + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security = &SecuritySpec{ + HostHeader: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security.HostHeader, + SecretToken: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.Security.SecretToken, + } + } + if src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse != nil { + dst.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse = &GatewayResponseSpec{ + ErrorStatusAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusAuthFailed, + ErrorHeadersAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersAuthFailed, + ErrorAuthFailed: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorAuthFailed, + ErrorStatusAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusAuthMissing, + ErrorHeadersAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersAuthMissing, + ErrorAuthMissing: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorAuthMissing, + ErrorStatusNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusNoMatch, + ErrorHeadersNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersNoMatch, + ErrorNoMatch: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorNoMatch, + ErrorStatusLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorStatusLimitsExceeded, + ErrorHeadersLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorHeadersLimitsExceeded, + ErrorLimitsExceeded: src.Spec.Deployment.ApicastHosted.Authentication.OIDC.GatewayResponse.ErrorLimitsExceeded, + } + } + } + } + } + + dst.Spec.MappingRules = []MappingRuleSpec{} + for i := range src.Spec.MappingRules { + dst.Spec.MappingRules = append(dst.Spec.MappingRules, MappingRuleSpec{ + HTTPMethod: src.Spec.MappingRules[i].HTTPMethod, + Pattern: src.Spec.MappingRules[i].Pattern, + MetricMethodRef: src.Spec.MappingRules[i].MetricMethodRef, + Increment: src.Spec.MappingRules[i].Increment, + Last: src.Spec.MappingRules[i].Last, + }) + } + + dst.Spec.BackendUsages = map[string]BackendUsageSpec{} + for s := range src.Spec.BackendUsages { + dst.Spec.BackendUsages[s] = BackendUsageSpec{ + Path: src.Spec.BackendUsages[s].Path, + } + } + + dst.Spec.Metrics = map[string]MetricSpec{} + for s := range src.Spec.Metrics { + dst.Spec.Metrics[s] = MetricSpec{ + Name: src.Spec.Metrics[s].Name, + Unit: src.Spec.Metrics[s].Unit, + Description: src.Spec.Metrics[s].Description, + } + } + + dst.Spec.Methods = map[string]MethodSpec{} + for s := range src.Spec.Methods { + dst.Spec.Methods[s] = MethodSpec{ + Name: src.Spec.Methods[s].Name, + Description: src.Spec.Methods[s].Description, + } + } + + dst.Spec.ApplicationPlans = map[string]ApplicationPlanSpec{} + for s := range src.Spec.ApplicationPlans { + plan := ApplicationPlanSpec{ + Name: src.Spec.ApplicationPlans[s].Name, + AppsRequireApproval: src.Spec.ApplicationPlans[s].AppsRequireApproval, + TrialPeriod: src.Spec.ApplicationPlans[s].TrialPeriod, + SetupFee: src.Spec.ApplicationPlans[s].SetupFee, + CostMonth: src.Spec.ApplicationPlans[s].CostMonth, + PricingRules: nil, + Limits: nil, + Published: src.Spec.ApplicationPlans[s].Published, + } + + for i := range src.Spec.ApplicationPlans[s].PricingRules { + plan.PricingRules = append(plan.PricingRules, PricingRuleSpec{ + From: src.Spec.ApplicationPlans[s].PricingRules[i].From, + To: src.Spec.ApplicationPlans[s].PricingRules[i].To, + MetricMethodRef: MetricMethodRefSpec{ + SystemName: src.Spec.ApplicationPlans[s].PricingRules[i].MetricMethodRef.SystemName, + BackendSystemName: src.Spec.ApplicationPlans[s].PricingRules[i].MetricMethodRef.BackendSystemName, + }, + PricePerUnit: src.Spec.ApplicationPlans[s].PricingRules[i].PricePerUnit, + }) + } + + dst.Spec.ApplicationPlans[s] = plan + } + + dst.Spec.ProviderAccountRef = src.Spec.ProviderAccountRef + + for i := range src.Spec.Policies { + dst.Spec.Policies = append(dst.Spec.Policies, PolicyConfig{ + Name: src.Spec.Policies[i].Name, + Version: src.Spec.Policies[i].Version, + Configuration: src.Spec.Policies[i].Configuration.Value, + Enabled: src.Spec.Policies[i].Enabled, + }) + } + + // Set the status + dst.Status.ID = src.Status.ID + dst.Status.State = src.Status.State + dst.Status.ProviderAccountHost = src.Status.ProviderAccountHost + dst.Status.ObservedGeneration = src.Status.ObservedGeneration + dst.Status.Conditions = src.Status.Conditions + return nil +} diff --git a/apis/capabilities/v1beta1/zz_generated.deepcopy.go b/apis/capabilities/v1beta1/zz_generated.deepcopy.go index 77fc9e812..6a8cbe671 100644 --- a/apis/capabilities/v1beta1/zz_generated.deepcopy.go +++ b/apis/capabilities/v1beta1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1beta1 import ( + "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -550,14 +551,14 @@ func (in *BackendSpec) DeepCopyInto(out *BackendSpec) { } if in.Metrics != nil { in, out := &in.Metrics, &out.Metrics - *out = make(map[string]MetricSpec, len(*in)) + *out = make(map[string]v1beta2.MetricSpec, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.Methods != nil { in, out := &in.Methods, &out.Methods - *out = make(map[string]MethodSpec, len(*in)) + *out = make(map[string]v1beta2.MethodSpec, len(*in)) for key, val := range *in { (*out)[key] = val } diff --git a/apis/capabilities/v1beta2/groupversion_info.go b/apis/capabilities/v1beta2/groupversion_info.go new file mode 100644 index 000000000..d795bde50 --- /dev/null +++ b/apis/capabilities/v1beta2/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2020 Red Hat. + +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 v1beta2 contains API Schema definitions for the capabilities v1beta2 API group +// +kubebuilder:object:generate=true +// +groupName=capabilities.3scale.net +package v1beta2 + +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: "capabilities.3scale.net", Version: "v1beta2"} + + // 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/apis/capabilities/v1beta2/product_conversion.go b/apis/capabilities/v1beta2/product_conversion.go new file mode 100644 index 000000000..d33c80770 --- /dev/null +++ b/apis/capabilities/v1beta2/product_conversion.go @@ -0,0 +1,4 @@ +package v1beta2 + +// Hub marks this type as a conversion hub. +func (*Product) Hub() {} diff --git a/apis/capabilities/v1beta2/product_types.go b/apis/capabilities/v1beta2/product_types.go new file mode 100644 index 000000000..c0f6e2739 --- /dev/null +++ b/apis/capabilities/v1beta2/product_types.go @@ -0,0 +1,1463 @@ +/* +Copyright 2020 Red Hat. + +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 v1beta2 + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/3scale/3scale-operator/pkg/common" + "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// 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. + +const ( + ProductKind = "Product" + + // ProductInvalidConditionType represents that the combination of configuration in the ProductSpec + // is not supported. This is not a transient error, but + // indicates a state that must be fixed before progress can be made. + // Example: the ProductSpec references non existing internal Metric reference + ProductInvalidConditionType common.ConditionType = "Invalid" + + // ProductOrphanConditionType represents that the configuration in the ProductSpec + // contains reference to non existing resource. + // This is (should be) a transient error, but + // indicates a state that must be fixed before progress can be made. + // Example: the ProductSpec references non existing backend resource + ProductOrphanConditionType common.ConditionType = "Orphan" + + // ProductSyncedConditionType indicates the product has been successfully synchronized. + // Steady state + ProductSyncedConditionType common.ConditionType = "Synced" + + // ProductFailedConditionType indicates that an error occurred during synchronization. + // The operator will retry. + ProductFailedConditionType common.ConditionType = "Failed" +) + +var ( + // apicastPolicy refers to the main functionality of APIcast to work with the 3scale API manager + // Needs to exist in the policy chain + apicastPolicy = PolicyConfig{ + Name: "apicast", + Version: "builtin", + Configuration: Configuration{ + Value: runtime.RawExtension{ + Raw: []byte(`{}`), + }, + ValueFrom: corev1.SecretReference{}, + }, + Enabled: true, + } +) + +var ( + // + productSystemNameRegexp = regexp.MustCompile("[^a-zA-Z0-9]+") +) + +// MetricMethodRefSpec defines method or metric reference +// Metric or method can optionally belong to used backends +type MetricMethodRefSpec struct { + // SystemName identifies uniquely the metric or methods + SystemName string `json:"systemName"` + + // BackendSystemName identifies uniquely the backend + // Backend reference must be used by the product + // +optional + BackendSystemName *string `json:"backend,omitempty"` +} + +func (m *MetricMethodRefSpec) String() string { + backendPrefix := "" + if m.BackendSystemName != nil { + backendPrefix = fmt.Sprintf("%s.", *m.BackendSystemName) + } + return fmt.Sprintf("%s%s", backendPrefix, m.SystemName) +} + +// LimitSpec defines the maximum value a metric can take on a contract before the user is no longer authorized to use resources. +// Once a limit has been passed in a given period, reject messages will be issued if the service is accessed under this contract. +type LimitSpec struct { + // Limit Period + // +kubebuilder:validation:Enum=eternity;year;month;week;day;hour;minute + Period string `json:"period"` + + // Limit Value + Value int `json:"value"` + + // Metric or Method Reference + MetricMethodRef MetricMethodRefSpec `json:"metricMethodRef"` +} + +// PricingRuleSpec defines the cost of each operation performed on an API. +// Multiple pricing rules on the same metric divide up the ranges of when a pricing rule applies. +type PricingRuleSpec struct { + // Range From + From int `json:"from"` + + // Range To + To int `json:"to"` + + // Metric or Method Reference + MetricMethodRef MetricMethodRefSpec `json:"metricMethodRef"` + + // Price per unit (USD) + // +kubebuilder:validation:Pattern=`^\d+(\.\d{2})?$` + PricePerUnit string `json:"pricePerUnit"` +} + +// ApplicationPlanSpec defines the desired state of Product's Application Plan +type ApplicationPlanSpec struct { + // +optional + Name *string `json:"name,omitempty"` + + // Set whether or not applications can be created on demand + // or if approval is required from you before they are activated. + // +optional + AppsRequireApproval *bool `json:"appsRequireApproval,omitempty"` + + // Trial Period (days) + // +kubebuilder:validation:Minimum=0 + // +optional + TrialPeriod *int `json:"trialPeriod,omitempty"` + + // Setup fee (USD) + // +kubebuilder:validation:Pattern=`^\d+(\.\d{2})?$` + // +optional + SetupFee *string `json:"setupFee,omitempty"` + + // Cost per Month (USD) + // +kubebuilder:validation:Pattern=`^\d+(\.\d{2})?$` + // +optional + CostMonth *string `json:"costMonth,omitempty"` + + // Pricing Rules + // +optional + PricingRules []PricingRuleSpec `json:"pricingRules,omitempty"` + + // Limits + // +optional + Limits []LimitSpec `json:"limits,omitempty"` + + // Controls whether the application plan is published. If not specified it is + // hidden by default + // +optional + Published *bool `json:"published,omitempty"` + + // TODO Features +} + +func (a *ApplicationPlanSpec) IsPublished() bool { + return a.Published != nil && *a.Published +} + +// MethodSpec defines the desired state of Product's Method +type MethodSpec struct { + Name string `json:"friendlyName"` + // +optional + Description string `json:"description,omitempty"` +} + +// MetricSpec defines the desired state of Product's Metric +type MetricSpec struct { + Name string `json:"friendlyName"` + Unit string `json:"unit"` + // +optional + Description string `json:"description,omitempty"` +} + +// MappingRuleSpec defines the desired state of Product's MappingRule +type MappingRuleSpec struct { + // +kubebuilder:validation:Enum=GET;HEAD;POST;PUT;DELETE;OPTIONS;TRACE;PATCH;CONNECT + HTTPMethod string `json:"httpMethod"` + Pattern string `json:"pattern"` + MetricMethodRef string `json:"metricMethodRef"` + Increment int `json:"increment"` + // +optional + Last *bool `json:"last,omitempty"` +} + +// BackendUsageSpec defines the desired state of Product's Backend Usages +type BackendUsageSpec struct { + Path string `json:"path"` +} + +// SecuritySpec defines the desired state of Authentication Security +type SecuritySpec struct { + // HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + // +optional + HostHeader *string `json:"hostHeader,omitempty"` + + // SecretToken Enables you to block any direct developer requests to your API backend; + // each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. + // The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + // +optional + SecretToken *string `json:"secretToken,omitempty"` +} + +func (s *SecuritySpec) SecuritySecretToken() *string { + return s.SecretToken +} + +func (s *SecuritySpec) HostRewrite() *string { + return s.HostHeader +} + +// AppKeyAppIDAuthenticationSpec defines the desired state of AppKey&AppId Authentication +type AppKeyAppIDAuthenticationSpec struct { + // AppID is the name of the parameter that acts of behalf of app id + // +optional + AppID *string `json:"appID,omitempty"` + + // AppKey is the name of the parameter that acts of behalf of app key + // +optional + AppKey *string `json:"appKey,omitempty"` + + // CredentialsLoc available options: + // headers: As HTTP Headers + // query: As query parameters (GET) or body parameters (POST/PUT/DELETE) + // authorization: As HTTP Basic Authentication + // +optional + // +kubebuilder:validation:Enum=headers;query;authorization + CredentialsLoc *string `json:"credentials,omitempty"` + + // +optional + Security *SecuritySpec `json:"security,omitempty"` + + // +optional + GatewayResponse *GatewayResponseSpec `json:"gatewayResponse,omitempty"` +} + +func (a *AppKeyAppIDAuthenticationSpec) SecuritySecretToken() *string { + if a.Security == nil { + return nil + } + + return a.Security.SecuritySecretToken() +} + +func (a *AppKeyAppIDAuthenticationSpec) HostRewrite() *string { + if a.Security == nil { + return nil + } + + return a.Security.HostRewrite() +} + +func (a *AppKeyAppIDAuthenticationSpec) CredentialsLocation() *string { + return a.CredentialsLoc +} + +func (a *AppKeyAppIDAuthenticationSpec) AuthAppID() *string { + return a.AppID +} + +func (a *AppKeyAppIDAuthenticationSpec) AuthAppKey() *string { + return a.AppKey +} + +func (a *AppKeyAppIDAuthenticationSpec) GatewayResponseSpec() *GatewayResponseSpec { + return a.GatewayResponse +} + +// UserKeyAuthenticationSpec defines the desired state of User Key Authentication +type UserKeyAuthenticationSpec struct { + // +optional + Key *string `json:"authUserKey,omitempty"` + + // Credentials Location available options: + // headers: As HTTP Headers + // query: As query parameters (GET) or body parameters (POST/PUT/DELETE) + // authorization: As HTTP Basic Authentication + // +optional + // +kubebuilder:validation:Enum=headers;query;authorization + CredentialsLoc *string `json:"credentials,omitempty"` + + // +optional + Security *SecuritySpec `json:"security,omitempty"` + + // +optional + GatewayResponse *GatewayResponseSpec `json:"gatewayResponse,omitempty"` +} + +func (u *UserKeyAuthenticationSpec) SecuritySecretToken() *string { + if u.Security == nil { + return nil + } + + return u.Security.SecuritySecretToken() +} + +func (u *UserKeyAuthenticationSpec) HostRewrite() *string { + if u.Security == nil { + return nil + } + + return u.Security.HostRewrite() +} + +func (u *UserKeyAuthenticationSpec) CredentialsLocation() *string { + return u.CredentialsLoc +} + +func (u *UserKeyAuthenticationSpec) AuthUserKey() *string { + return u.Key +} + +func (u *UserKeyAuthenticationSpec) GatewayResponseSpec() *GatewayResponseSpec { + return u.GatewayResponse +} + +// OIDCAuthenticationFlowSpec defines the desired OAuth2.0 authorization grant type +type OIDCAuthenticationFlowSpec struct { + // OIDCIssuer is the OIDC issuer + StandardFlowEnabled bool `json:"standardFlowEnabled"` + ImplicitFlowEnabled bool `json:"implicitFlowEnabled"` + ServiceAccountsEnabled bool `json:"serviceAccountsEnabled"` + DirectAccessGrantsEnabled bool `json:"directAccessGrantsEnabled"` +} + +// OIDCSpec defines the desired configuration of OpenID Connect Authentication +type OIDCSpec struct { + // IssuerType is the type of the OIDC issuer + // +kubebuilder:validation:Enum=keycloak;rest + IssuerType string `json:"issuerType"` + + // Issuer is the OIDC issuer + IssuerEndpoint string `json:"issuerEndpoint"` + + // AuthenticationFlow specifies OAuth2.0 authorization grant type + // +optional + AuthenticationFlow *OIDCAuthenticationFlowSpec `json:"authenticationFlow,omitempty"` + + // JwtClaimWithClientID is the JSON Web Token (JWT) Claim with ClientID that contains the clientID. Defaults to 'azp'. + // +optional + JwtClaimWithClientID *string `json:"jwtClaimWithClientID,omitempty"` + + // JwtClaimWithClientIDType sets to process the ClientID Token Claim value as a string or as a liquid template. + // +kubebuilder:validation:Enum=plain;liquid + // +optional + JwtClaimWithClientIDType *string `json:"jwtClaimWithClientIDType,omitempty"` + + // Credentials Location available options: + // headers: As HTTP Headers + // query: As query parameters (GET) or body parameters (POST/PUT/DELETE) + // authorization: As HTTP Basic Authentication + // +optional + // +kubebuilder:validation:Enum=headers;query;authorization + CredentialsLoc *string `json:"credentials,omitempty"` + + // +optional + Security *SecuritySpec `json:"security,omitempty"` + + // +optional + GatewayResponse *GatewayResponseSpec `json:"gatewayResponse,omitempty"` +} + +func (u *OIDCSpec) SecuritySecretToken() *string { + if u.Security == nil { + return nil + } + + return u.Security.SecuritySecretToken() +} + +func (u *OIDCSpec) HostRewrite() *string { + if u.Security == nil { + return nil + } + + return u.Security.HostRewrite() +} + +func (u *OIDCSpec) CredentialsLocation() *string { + return u.CredentialsLoc +} + +func (u *OIDCSpec) GatewayResponseSpec() *GatewayResponseSpec { + return u.GatewayResponse +} + +// AuthenticationSpec defines the desired state of Product Authentication +type AuthenticationSpec struct { + // +optional + UserKeyAuthentication *UserKeyAuthenticationSpec `json:"userkey,omitempty"` + + // +optional + AppKeyAppIDAuthentication *AppKeyAppIDAuthenticationSpec `json:"appKeyAppID,omitempty"` + + // +optional + OIDC *OIDCSpec `json:"oidc,omitempty"` +} + +func (a *AuthenticationSpec) AuthenticationMode() string { + // authentication is oneOf by CRD openapiV3 validation + if a.UserKeyAuthentication != nil { + return "1" + } + + if a.AppKeyAppIDAuthentication != nil { + return "2" + } + + if a.OIDC != nil { + return "oidc" + } + + panic("product authenticationspec: all options are nil") +} + +func (a *AuthenticationSpec) SecuritySecretToken() *string { + // authentication is oneOf by CRD openapiV3 validation + if a.UserKeyAuthentication != nil { + return a.UserKeyAuthentication.SecuritySecretToken() + } + + if a.AppKeyAppIDAuthentication != nil { + return a.AppKeyAppIDAuthentication.SecuritySecretToken() + } + + if a.OIDC != nil { + return a.OIDC.SecuritySecretToken() + } + + panic("product authenticationspec: all options are nil") +} + +func (a *AuthenticationSpec) HostRewrite() *string { + // authentication is oneOf by CRD openapiV3 validation + if a.UserKeyAuthentication != nil { + return a.UserKeyAuthentication.HostRewrite() + } + + if a.AppKeyAppIDAuthentication != nil { + return a.AppKeyAppIDAuthentication.HostRewrite() + } + + if a.OIDC != nil { + return a.OIDC.HostRewrite() + } + + panic("product authenticationspec: all options are nil") +} + +func (a *AuthenticationSpec) CredentialsLocation() *string { + // authentication is oneOf by CRD openapiV3 validation + if a.UserKeyAuthentication != nil { + return a.UserKeyAuthentication.CredentialsLocation() + } + + if a.AppKeyAppIDAuthentication != nil { + return a.AppKeyAppIDAuthentication.CredentialsLocation() + } + + if a.OIDC != nil { + return a.OIDC.CredentialsLocation() + } + + panic("product authenticationspec: all options are nil") +} + +func (a *AuthenticationSpec) AuthUserKey() *string { + // authentication is oneOf by CRD openapiV3 validation + if a.UserKeyAuthentication != nil { + return a.UserKeyAuthentication.AuthUserKey() + } + + return nil +} + +func (a *AuthenticationSpec) AuthAppID() *string { + // authentication is oneOf by CRD openapiV3 validation + if a.AppKeyAppIDAuthentication != nil { + return a.AppKeyAppIDAuthentication.AuthAppID() + } + + return nil +} + +func (a *AuthenticationSpec) AuthAppKey() *string { + // authentication is oneOf by CRD openapiV3 validation + if a.AppKeyAppIDAuthentication != nil { + return a.AppKeyAppIDAuthentication.AuthAppKey() + } + + return nil +} + +func (a *AuthenticationSpec) GatewayResponse() *GatewayResponseSpec { + // authentication is oneOf by CRD openapiV3 validation + if a.UserKeyAuthentication != nil { + return a.UserKeyAuthentication.GatewayResponseSpec() + } + + if a.AppKeyAppIDAuthentication != nil { + return a.AppKeyAppIDAuthentication.GatewayResponseSpec() + } + + if a.OIDC != nil { + return a.OIDC.GatewayResponseSpec() + } + + panic("product authenticationspec: all options are nil") +} + +func (a *AuthenticationSpec) OIDCSpec() *OIDCSpec { + return a.OIDC +} + +// ApicastHostedSpec defines the desired state of Product Apicast Hosted +type ApicastHostedSpec struct { + // +optional + Authentication *AuthenticationSpec `json:"authentication,omitempty"` +} + +func (a *ApicastHostedSpec) AuthenticationMode() *string { + if a.Authentication == nil { + return nil + } + authenticationMode := a.Authentication.AuthenticationMode() + return &authenticationMode +} + +func (a *ApicastHostedSpec) SecuritySecretToken() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.SecuritySecretToken() +} + +func (a *ApicastHostedSpec) HostRewrite() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.HostRewrite() +} + +func (a *ApicastHostedSpec) CredentialsLocation() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.CredentialsLocation() +} + +func (a *ApicastHostedSpec) AuthUserKey() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.AuthUserKey() +} + +func (a *ApicastHostedSpec) AuthAppID() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.AuthAppID() +} + +func (a *ApicastHostedSpec) AuthAppKey() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.AuthAppKey() +} + +func (a *ApicastHostedSpec) GatewayResponse() *GatewayResponseSpec { + if a.Authentication == nil { + return nil + } + return a.Authentication.GatewayResponse() +} + +func (a *ApicastHostedSpec) OIDCSpec() *OIDCSpec { + if a.Authentication == nil { + return nil + } + return a.Authentication.OIDCSpec() +} + +// ApicastSelfManagedSpec defines the desired state of Product Apicast Self Managed +type ApicastSelfManagedSpec struct { + // +optional + Authentication *AuthenticationSpec `json:"authentication,omitempty"` + // +optional + // +kubebuilder:validation:Pattern=`^https?:\/\/.*$` + StagingPublicBaseURL *string `json:"stagingPublicBaseURL,omitempty"` + // +optional + // +kubebuilder:validation:Pattern=`^https?:\/\/.*$` + ProductionPublicBaseURL *string `json:"productionPublicBaseURL,omitempty"` +} + +func (a *ApicastSelfManagedSpec) AuthenticationMode() *string { + if a.Authentication == nil { + return nil + } + authenticationMode := a.Authentication.AuthenticationMode() + return &authenticationMode +} + +func (a *ApicastSelfManagedSpec) ProdPublicBaseURL() *string { + return a.ProductionPublicBaseURL +} + +func (a *ApicastSelfManagedSpec) StagPublicBaseURL() *string { + return a.StagingPublicBaseURL +} + +func (a *ApicastSelfManagedSpec) SecuritySecretToken() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.SecuritySecretToken() +} + +func (a *ApicastSelfManagedSpec) HostRewrite() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.HostRewrite() +} + +func (a *ApicastSelfManagedSpec) CredentialsLocation() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.CredentialsLocation() +} + +func (a *ApicastSelfManagedSpec) AuthUserKey() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.AuthUserKey() +} + +func (a *ApicastSelfManagedSpec) AuthAppID() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.AuthAppID() +} + +func (a *ApicastSelfManagedSpec) AuthAppKey() *string { + if a.Authentication == nil { + return nil + } + return a.Authentication.AuthAppKey() +} + +func (a *ApicastSelfManagedSpec) GatewayResponse() *GatewayResponseSpec { + if a.Authentication == nil { + return nil + } + return a.Authentication.GatewayResponse() +} + +func (a *ApicastSelfManagedSpec) OIDCSpec() *OIDCSpec { + if a.Authentication == nil { + return nil + } + return a.Authentication.OIDCSpec() +} + +// ProductDeploymentSpec defines the desired state of Product Deployment +type ProductDeploymentSpec struct { + // +optional + ApicastHosted *ApicastHostedSpec `json:"apicastHosted,omitempty"` + // +optional + ApicastSelfManaged *ApicastSelfManagedSpec `json:"apicastSelfManaged,omitempty"` +} + +func (d *ProductDeploymentSpec) DeploymentOption() string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return "hosted" + } + + // must be self managed + return "self_managed" +} + +func (d *ProductDeploymentSpec) AuthenticationMode() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.AuthenticationMode() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + // must be self managed, a + return d.ApicastSelfManaged.AuthenticationMode() +} + +func (d *ProductDeploymentSpec) ProdPublicBaseURL() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + // Hosted deployment mode does not allow updating public base urls + return nil + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.ProdPublicBaseURL() +} + +func (d *ProductDeploymentSpec) StagingPublicBaseURL() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + // Hosted deployment mode does not allow updating public base urls + return nil + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.StagPublicBaseURL() +} + +func (d *ProductDeploymentSpec) SecuritySecretToken() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.SecuritySecretToken() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.SecuritySecretToken() +} + +func (d *ProductDeploymentSpec) HostRewrite() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.HostRewrite() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.HostRewrite() +} + +func (d *ProductDeploymentSpec) CredentialsLocation() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.CredentialsLocation() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.CredentialsLocation() +} + +func (d *ProductDeploymentSpec) AuthUserKey() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.AuthUserKey() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.AuthUserKey() +} + +func (d *ProductDeploymentSpec) AuthAppID() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.AuthAppID() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.AuthAppID() +} + +func (d *ProductDeploymentSpec) AuthAppKey() *string { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.AuthAppKey() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.AuthAppKey() +} + +func (d *ProductDeploymentSpec) GatewayResponse() *GatewayResponseSpec { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.GatewayResponse() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.GatewayResponse() +} + +// PolicyConfig defines policy definition +type PolicyConfig struct { + // Name defines the policy unique name + Name string `json:"name"` + + // Version defines the policy version + Version string `json:"version"` + + // Configuration defines the policy configuration + Configuration Configuration `json:"configuration"` + + // Enabled defines activation state + Enabled bool `json:"enabled"` +} + +type Configuration struct { + // +kubebuilder:pruning:PreserveUnknownFields + Value runtime.RawExtension `json:"value,omitempty"` + + // ValueFrom defines the secret where the configuration can be retrieved + ValueFrom corev1.SecretReference `json:"valueFrom,omitempty"` +} + +func (d *ProductDeploymentSpec) OIDCSpec() *OIDCSpec { + // spec.deployment is oneOf by CRD openapiV3 validation + if d.ApicastHosted != nil { + return d.ApicastHosted.OIDCSpec() + } + + if d.ApicastSelfManaged == nil { + panic("product spec.deployment apicasthosted and selfmanaged are nil") + } + + return d.ApicastSelfManaged.OIDCSpec() +} + +// ProductSpec defines the desired state of Product +type ProductSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Name is human readable name for the product + Name string `json:"name"` + + // SystemName identifies uniquely the product within the account provider + // Default value will be sanitized Name + // +optional + SystemName string `json:"systemName,omitempty"` + + // Description is a human readable text of the product + // +optional + Description string `json:"description,omitempty"` + + // Deployment defined 3scale product deployment mode + // +optional + Deployment *ProductDeploymentSpec `json:"deployment,omitempty"` + + // Mapping Rules + // Array: MappingRule Spec + // +optional + MappingRules []MappingRuleSpec `json:"mappingRules,omitempty"` + + // Backend usage will be a map of + // Map: system_name -> BackendUsageSpec + // Having system_name as the index, the structure ensures one backend is not used multiple times. + // +optional + BackendUsages map[string]BackendUsageSpec `json:"backendUsages,omitempty"` + + // Metrics + // Map: system_name -> MetricSpec + // system_name attr is unique for all metrics AND methods + // In other words, if metric's system_name is A, there is no metric or method with system_name A. + // +optional + Metrics map[string]MetricSpec `json:"metrics,omitempty"` + + // Methods + // Map: system_name -> MethodSpec + // system_name attr is unique for all metrics AND methods + // In other words, if metric's system_name is A, there is no metric or method with system_name A. + // +optional + Methods map[string]MethodSpec `json:"methods,omitempty"` + + // Application Plans + // Map: system_name -> Application Plan Spec + // +optional + ApplicationPlans map[string]ApplicationPlanSpec `json:"applicationPlans,omitempty"` + + // ProviderAccountRef references account provider credentials + // +optional + ProviderAccountRef *corev1.LocalObjectReference `json:"providerAccountRef,omitempty"` + + // Policies holds the product's policy chain + // +optional + Policies []PolicyConfig `json:"policies,omitempty"` +} + +func (s *ProductSpec) DeploymentOption() *string { + if s.Deployment == nil { + return nil + } + deploymentOption := s.Deployment.DeploymentOption() + return &deploymentOption +} + +func (s *ProductSpec) AuthenticationMode() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.AuthenticationMode() +} + +func (s *ProductSpec) ProdPublicBaseURL() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.ProdPublicBaseURL() +} + +func (s *ProductSpec) StagingPublicBaseURL() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.StagingPublicBaseURL() +} + +func (s *ProductSpec) SecuritySecretToken() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.SecuritySecretToken() +} + +func (s *ProductSpec) HostRewrite() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.HostRewrite() +} + +func (s *ProductSpec) CredentialsLocation() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.CredentialsLocation() +} + +func (s *ProductSpec) AuthUserKey() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.AuthUserKey() +} + +func (s *ProductSpec) AuthAppID() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.AuthAppID() +} + +func (s *ProductSpec) AuthAppKey() *string { + if s.Deployment == nil { + return nil + } + return s.Deployment.AuthAppKey() +} + +func (s *ProductSpec) GatewayResponse() *GatewayResponseSpec { + if s.Deployment == nil { + return nil + } + return s.Deployment.GatewayResponse() +} + +func (s *ProductSpec) OIDCSpec() *OIDCSpec { + if s.Deployment == nil { + return nil + } + return s.Deployment.OIDCSpec() +} + +// GatewayResponseSpec defines the desired gateway response configuration +type GatewayResponseSpec struct { + // ErrorStatusAuthFailed specifies the response code when authentication fails + // +optional + ErrorStatusAuthFailed *int32 `json:"errorStatusAuthFailed,omitempty"` + + // ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + // +optional + ErrorHeadersAuthFailed *string `json:"errorHeadersAuthFailed,omitempty"` + + // ErrorAuthFailed specifies the response body when authentication fails + // +optional + ErrorAuthFailed *string `json:"errorAuthFailed,omitempty"` + + // ErrorStatusAuthMissing specifies the response code when authentication is missing + // +optional + ErrorStatusAuthMissing *int32 `json:"errorStatusAuthMissing,omitempty"` + + // ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + // +optional + ErrorHeadersAuthMissing *string `json:"errorHeadersAuthMissing,omitempty"` + + // ErrorAuthMissing specifies the response body when authentication is missing + // +optional + ErrorAuthMissing *string `json:"errorAuthMissing,omitempty"` + + // ErrorStatusNoMatch specifies the response code when no match error + // +optional + ErrorStatusNoMatch *int32 `json:"errorStatusNoMatch,omitempty"` + + // ErrorHeadersNoMatch specifies the Content-Type header when no match error + // +optional + ErrorHeadersNoMatch *string `json:"errorHeadersNoMatch,omitempty"` + + // ErrorNoMatch specifies the response body when no match error + // +optional + ErrorNoMatch *string `json:"errorNoMatch,omitempty"` + + // ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + // +optional + ErrorStatusLimitsExceeded *int32 `json:"errorStatusLimitsExceeded,omitempty"` + + // ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + // +optional + ErrorHeadersLimitsExceeded *string `json:"errorHeadersLimitsExceeded,omitempty"` + + // ErrorLimitsExceeded specifies the response body when usage limit exceeded + // +optional + ErrorLimitsExceeded *string `json:"errorLimitsExceeded,omitempty"` +} + +// ProductStatus defines the observed state of Product +type ProductStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // +optional + ID *int64 `json:"productId,omitempty"` + // +optional + State *string `json:"state,omitempty"` + + // 3scale control plane host + // +optional + ProviderAccountHost string `json:"providerAccountHost,omitempty"` + + // ObservedGeneration reflects the generation of the most recently observed Product Spec. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Current state of the 3scale product. + // Conditions represent the latest available observations of an object's state + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions common.Conditions `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=conditions"` +} + +func (p *ProductStatus) Equals(other *ProductStatus, logger logr.Logger) bool { + if !reflect.DeepEqual(p.ID, other.ID) { + diff := cmp.Diff(p.ID, other.ID) + logger.V(1).Info("ID not equal", "difference", diff) + return false + } + + if !reflect.DeepEqual(p.State, other.State) { + diff := cmp.Diff(p.State, other.State) + logger.V(1).Info("State not equal", "difference", diff) + return false + } + + if p.ProviderAccountHost != other.ProviderAccountHost { + diff := cmp.Diff(p.ProviderAccountHost, other.ProviderAccountHost) + logger.V(1).Info("ProviderAccountHost not equal", "difference", diff) + return false + } + + if p.ObservedGeneration != other.ObservedGeneration { + diff := cmp.Diff(p.ObservedGeneration, other.ObservedGeneration) + logger.V(1).Info("ObservedGeneration not equal", "difference", diff) + return false + } + + // Marshalling sorts by condition type + currentMarshaledJSON, _ := p.Conditions.MarshalJSON() + otherMarshaledJSON, _ := other.Conditions.MarshalJSON() + if string(currentMarshaledJSON) != string(otherMarshaledJSON) { + diff := cmp.Diff(string(currentMarshaledJSON), string(otherMarshaledJSON)) + logger.V(1).Info("Conditions not equal", "difference", diff) + return false + } + + return true +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:storageversion + +// Product is the Schema for the products API +// +kubebuilder:resource:path=products,scope=Namespaced +// +operator-sdk:csv:customresourcedefinitions:displayName="3scale Product" +type Product struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ProductSpec `json:"spec,omitempty"` + Status ProductStatus `json:"status,omitempty"` +} + +// RemoveBackendReferences returns true if product CR has mentions of a backend that matches +// backendSystemName in: backendUsage, Pricing Plans, Limits and removes the references of backend. +func (product *Product) RemoveBackendReferences(backendSystemName string) bool { + removalDone := false + + if _, ok := product.Spec.BackendUsages[backendSystemName]; ok { + delete(product.Spec.BackendUsages, backendSystemName) + removalDone = true + } + + for appIDX, applicationPlan := range product.Spec.ApplicationPlans { + currentApplicationPlan := applicationPlan + + // remove pricing rules that mention the systemName + for pricingRuleIDX, pricingRule := range applicationPlan.PricingRules { + if pricingRule.MetricMethodRef.BackendSystemName != nil && *pricingRule.MetricMethodRef.BackendSystemName == backendSystemName { + currentApplicationPlan.PricingRules = removePricingRule(applicationPlan.PricingRules, pricingRuleIDX) + removalDone = true + } + } + + // remove limit rules that mention the systemName + for limitRuleIDX, limitRule := range applicationPlan.Limits { + if limitRule.MetricMethodRef.BackendSystemName != nil && *limitRule.MetricMethodRef.BackendSystemName == backendSystemName { + currentApplicationPlan.Limits = removeLimitRule(applicationPlan.Limits, limitRuleIDX) + removalDone = true + } + } + + product.Spec.ApplicationPlans[appIDX] = currentApplicationPlan + } + + return removalDone +} + +func (product *Product) SetDefaults(logger logr.Logger) bool { + updated := false + + // Respect 3scale API defaults + if product.Spec.SystemName == "" { + product.Spec.SystemName = productSystemNameRegexp.ReplaceAllString(product.Spec.Name, "") + updated = true + } + + // 3scale API ignores case of the system name field + systemNameLowercase := strings.ToLower(product.Spec.SystemName) + if product.Spec.SystemName != systemNameLowercase { + logger.Info("System name updated", "from", product.Spec.SystemName, "to", systemNameLowercase) + product.Spec.SystemName = systemNameLowercase + updated = true + } + + if product.Spec.Metrics == nil { + product.Spec.Metrics = map[string]MetricSpec{} + updated = true + } + + // Hits metric + hitsFound := false + for systemName := range product.Spec.Metrics { + if systemName == "hits" { + hitsFound = true + } + } + if !hitsFound { + logger.Info("Hits metric added") + product.Spec.Metrics["hits"] = MetricSpec{ + Name: "Hits", + Unit: "hit", + Description: "Number of API hits", + } + updated = true + } + + // Apicast Policy must exist + apicastPolicyFound := false + for idx := range product.Spec.Policies { + if product.Spec.Policies[idx].Name == apicastPolicy.Name { + apicastPolicyFound = true + break + } + } + + if !apicastPolicyFound { + // Add to the end of the slice as the one with the lowest priority + product.Spec.Policies = append(product.Spec.Policies, apicastPolicy) + updated = true + } + + return updated +} + +func (product *Product) Validate() field.ErrorList { + errors := field.ErrorList{} + specFldPath := field.NewPath("spec") + metricsFldPath := specFldPath.Child("metrics") + mappingRulesFldPath := specFldPath.Child("mappingRules") + applicationPlansFldPath := specFldPath.Child("applicationPlans") + methodsFldPath := specFldPath.Child("methods") + + // check hits metric exists + if len(product.Spec.Metrics) == 0 { + errors = append(errors, field.Required(metricsFldPath, "Product spec does not allow empty metrics.")) + } else { + if _, ok := product.Spec.Metrics["hits"]; !ok { + errors = append(errors, field.Invalid(metricsFldPath, nil, "metrics map not valid for Product. 'hits' metric must exist.")) + } + } + + metricSystemNameMap := map[string]interface{}{} + // Check metric systemNames are unique for all metric and methods + for systemName := range product.Spec.Metrics { + if _, ok := metricSystemNameMap[systemName]; ok { + metricIdxFldPath := metricsFldPath.Key(systemName) + errors = append(errors, field.Invalid(metricIdxFldPath, product.Spec.Metrics[systemName], "metric system_name not unique.")) + } else { + metricSystemNameMap[systemName] = nil + } + } + // Check method systemNames are unique for all metric and methods + for systemName := range product.Spec.Methods { + if _, ok := metricSystemNameMap[systemName]; ok { + methodIdxFldPath := methodsFldPath.Key(systemName) + errors = append(errors, field.Invalid(methodIdxFldPath, product.Spec.Methods[systemName], "method system_name not unique.")) + } else { + metricSystemNameMap[systemName] = nil + } + } + + metricFirendlyNameMap := map[string]interface{}{} + // Check metric names are unique for all metric and methods + for systemName, metricSpec := range product.Spec.Metrics { + if _, ok := metricFirendlyNameMap[metricSpec.Name]; ok { + metricIdxFldPath := metricsFldPath.Key(systemName) + errors = append(errors, field.Invalid(metricIdxFldPath, metricSpec, "metric name not unique.")) + } else { + metricFirendlyNameMap[systemName] = nil + } + } + // Check method names are unique for all metric and methods + for systemName, methodSpec := range product.Spec.Methods { + if _, ok := metricFirendlyNameMap[methodSpec.Name]; ok { + methodIdxFldPath := methodsFldPath.Key(systemName) + errors = append(errors, field.Invalid(methodIdxFldPath, methodSpec, "method name not unique.")) + } else { + metricFirendlyNameMap[systemName] = nil + } + } + + // Check mapping rules metrics and method refs exists + for idx, spec := range product.Spec.MappingRules { + if !product.FindMetricOrMethod(spec.MetricMethodRef) { + mappingRulesIdxFldPath := mappingRulesFldPath.Index(idx) + errors = append(errors, field.Invalid(mappingRulesIdxFldPath, spec.MetricMethodRef, "mappingrule does not have valid metric or method reference.")) + } + } + + // Check application plan limits local metricOrMethod ref exists + for planSystemName, planSpec := range product.Spec.ApplicationPlans { + planFldPath := applicationPlansFldPath.Key(planSystemName) + limitsFldPath := planFldPath.Child("limits") + for idx, limitSpec := range planSpec.Limits { + // Only local references + if limitSpec.MetricMethodRef.BackendSystemName == nil && !product.FindMetricOrMethod(limitSpec.MetricMethodRef.SystemName) { + limitFldPath := limitsFldPath.Index(idx) + metricRefFldPath := limitFldPath.Child("metricMethodRef") + errors = append(errors, field.Invalid(metricRefFldPath, limitSpec.MetricMethodRef.SystemName, "limit does not have valid local metric or method reference.")) + } + } + } + + // Check application plan limits keys (periods, metric) are unique + for planSystemName, planSpec := range product.Spec.ApplicationPlans { + planFldPath := applicationPlansFldPath.Key(planSystemName) + limitsFldPath := planFldPath.Child("limits") + periods := map[string]interface{}{} + for idx, limitSpec := range planSpec.Limits { + key := fmt.Sprintf("%s:%s", limitSpec.Period, limitSpec.MetricMethodRef.String()) + if _, ok := periods[key]; ok { + limitFldPath := limitsFldPath.Index(idx) + errors = append(errors, field.Invalid(limitFldPath, key, "limit period is not unique for the same metric.")) + } else { + periods[key] = nil + } + } + } + + // Check application plan pricing rule local metricOrMethod ref exists + for planSystemName, planSpec := range product.Spec.ApplicationPlans { + planFldPath := applicationPlansFldPath.Key(planSystemName) + rulesFldPath := planFldPath.Child("pricingRules") + for idx, pruleSpec := range planSpec.PricingRules { + // Only local references + if pruleSpec.MetricMethodRef.BackendSystemName == nil && !product.FindMetricOrMethod(pruleSpec.MetricMethodRef.SystemName) { + ruleFldPath := rulesFldPath.Index(idx) + metricRefFldPath := ruleFldPath.Child("metricMethodRef") + errors = append(errors, field.Invalid(metricRefFldPath, pruleSpec.MetricMethodRef.SystemName, "Pricing rule does not have valid local metric or method reference.")) + } + } + } + + // Check application plan pricing rules From < To + for planSystemName, planSpec := range product.Spec.ApplicationPlans { + planFldPath := applicationPlansFldPath.Key(planSystemName) + rulesFldPath := planFldPath.Child("pricingRules") + for idx, ruleSpec := range planSpec.PricingRules { + if ruleSpec.From > ruleSpec.To { + ruleFldPath := rulesFldPath.Index(idx) + bytes, _ := json.Marshal(ruleSpec) + errors = append(errors, field.Invalid(ruleFldPath, string(bytes), "'To' value cannot be less than your 'From' value.")) + } + } + } + + // Check application plan pricing rules ranges are not overlapping + for planSystemName, planSpec := range product.Spec.ApplicationPlans { + planFldPath := applicationPlansFldPath.Key(planSystemName) + rulesFldPath := planFldPath.Child("pricingRules") + overlappedIndex := detectOverlappingPricingRuleRanges(planSpec.PricingRules) + if overlappedIndex >= 0 { + ruleFldPath := rulesFldPath.Index(overlappedIndex) + bytes, _ := json.Marshal(planSpec.PricingRules[overlappedIndex]) + errors = append(errors, field.Invalid(ruleFldPath, string(bytes), "'From' value cannot be less than 'To' values of current rules for the same metric.")) + } + } + + return errors +} + +func (product *Product) IsSynced() bool { + return product.Status.Conditions.IsTrueFor(ProductSyncedConditionType) +} + +func (product *Product) FindMetricOrMethod(ref string) bool { + if len(product.Spec.Metrics) > 0 { + if _, ok := product.Spec.Metrics[ref]; ok { + return true + } + } + + if len(product.Spec.Methods) > 0 { + if _, ok := product.Spec.Methods[ref]; ok { + return true + } + } + + return false +} + +func detectOverlappingPricingRuleRanges(rules []PricingRuleSpec) int { + rulesPerMetricMap := make(map[string][]PricingRuleSpec) + for _, spec := range rules { + key := spec.MetricMethodRef.String() + rulesPerMetricMap[key] = append(rulesPerMetricMap[key], spec) + } + + for _, rulesPerMetric := range rulesPerMetricMap { + idx := isOverlappingRanges(rulesPerMetric) + if idx >= 0 { + ruleSpec := rulesPerMetric[idx] + // search and return index + for idx, spec := range rules { + if spec == ruleSpec { + return idx + } + } + } + } + return -1 +} + +func isOverlappingRanges(rules []PricingRuleSpec) int { + // Naive implementation: check rule X with all predecessors if there is overlapping + if len(rules) < 2 { + return -1 + } + + for a := 1; a < len(rules); a++ { + for b := 0; b < a; b++ { + // Assume for all rules From <= To + // Let Condition A Mean that ruleRange A Completely After ruleRange B: true if ToB <= FromA + // Let Condition B Mean that ruleRange A Completely Before ruleRange B: true if ToA <= FromB + // Then Overlap exists if Neither A Nor B is true + // ~(A or B) <=> ~a and ~b + // Thus: Overlap <=> ToB > FromA && ToA > FromB + if rules[a].From < rules[b].To && rules[a].To > rules[b].From { + return a + } + } + } + + return -1 +} + +// +kubebuilder:object:root=true + +// ProductList contains a list of Product +type ProductList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Product `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Product{}, &ProductList{}) +} + +func removePricingRule(s []PricingRuleSpec, index int) []PricingRuleSpec { + return append(s[:index], s[index+1:]...) +} + +func removeLimitRule(s []LimitSpec, index int) []LimitSpec { + return append(s[:index], s[index+1:]...) +} diff --git a/apis/capabilities/v1beta2/product_webhook.go b/apis/capabilities/v1beta2/product_webhook.go new file mode 100644 index 000000000..e703673b4 --- /dev/null +++ b/apis/capabilities/v1beta2/product_webhook.go @@ -0,0 +1,33 @@ +/* +Copyright 2020 Red Hat. + +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 v1beta2 + +import ( + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// log is for logging in this package. +var productlog = logf.Log.WithName("product-resource") + +func (r *Product) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! diff --git a/apis/capabilities/v1beta2/zz_generated.deepcopy.go b/apis/capabilities/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 000000000..3d698b5c4 --- /dev/null +++ b/apis/capabilities/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,735 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2020 Red Hat. + +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 v1beta2 + +import ( + "github.com/3scale/3scale-operator/pkg/common" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApicastHostedSpec) DeepCopyInto(out *ApicastHostedSpec) { + *out = *in + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(AuthenticationSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApicastHostedSpec. +func (in *ApicastHostedSpec) DeepCopy() *ApicastHostedSpec { + if in == nil { + return nil + } + out := new(ApicastHostedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApicastSelfManagedSpec) DeepCopyInto(out *ApicastSelfManagedSpec) { + *out = *in + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(AuthenticationSpec) + (*in).DeepCopyInto(*out) + } + if in.StagingPublicBaseURL != nil { + in, out := &in.StagingPublicBaseURL, &out.StagingPublicBaseURL + *out = new(string) + **out = **in + } + if in.ProductionPublicBaseURL != nil { + in, out := &in.ProductionPublicBaseURL, &out.ProductionPublicBaseURL + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApicastSelfManagedSpec. +func (in *ApicastSelfManagedSpec) DeepCopy() *ApicastSelfManagedSpec { + if in == nil { + return nil + } + out := new(ApicastSelfManagedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppKeyAppIDAuthenticationSpec) DeepCopyInto(out *AppKeyAppIDAuthenticationSpec) { + *out = *in + if in.AppID != nil { + in, out := &in.AppID, &out.AppID + *out = new(string) + **out = **in + } + if in.AppKey != nil { + in, out := &in.AppKey, &out.AppKey + *out = new(string) + **out = **in + } + if in.CredentialsLoc != nil { + in, out := &in.CredentialsLoc, &out.CredentialsLoc + *out = new(string) + **out = **in + } + if in.Security != nil { + in, out := &in.Security, &out.Security + *out = new(SecuritySpec) + (*in).DeepCopyInto(*out) + } + if in.GatewayResponse != nil { + in, out := &in.GatewayResponse, &out.GatewayResponse + *out = new(GatewayResponseSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppKeyAppIDAuthenticationSpec. +func (in *AppKeyAppIDAuthenticationSpec) DeepCopy() *AppKeyAppIDAuthenticationSpec { + if in == nil { + return nil + } + out := new(AppKeyAppIDAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationPlanSpec) DeepCopyInto(out *ApplicationPlanSpec) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.AppsRequireApproval != nil { + in, out := &in.AppsRequireApproval, &out.AppsRequireApproval + *out = new(bool) + **out = **in + } + if in.TrialPeriod != nil { + in, out := &in.TrialPeriod, &out.TrialPeriod + *out = new(int) + **out = **in + } + if in.SetupFee != nil { + in, out := &in.SetupFee, &out.SetupFee + *out = new(string) + **out = **in + } + if in.CostMonth != nil { + in, out := &in.CostMonth, &out.CostMonth + *out = new(string) + **out = **in + } + if in.PricingRules != nil { + in, out := &in.PricingRules, &out.PricingRules + *out = make([]PricingRuleSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Limits != nil { + in, out := &in.Limits, &out.Limits + *out = make([]LimitSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Published != nil { + in, out := &in.Published, &out.Published + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationPlanSpec. +func (in *ApplicationPlanSpec) DeepCopy() *ApplicationPlanSpec { + if in == nil { + return nil + } + out := new(ApplicationPlanSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationSpec) DeepCopyInto(out *AuthenticationSpec) { + *out = *in + if in.UserKeyAuthentication != nil { + in, out := &in.UserKeyAuthentication, &out.UserKeyAuthentication + *out = new(UserKeyAuthenticationSpec) + (*in).DeepCopyInto(*out) + } + if in.AppKeyAppIDAuthentication != nil { + in, out := &in.AppKeyAppIDAuthentication, &out.AppKeyAppIDAuthentication + *out = new(AppKeyAppIDAuthenticationSpec) + (*in).DeepCopyInto(*out) + } + if in.OIDC != nil { + in, out := &in.OIDC, &out.OIDC + *out = new(OIDCSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationSpec. +func (in *AuthenticationSpec) DeepCopy() *AuthenticationSpec { + if in == nil { + return nil + } + out := new(AuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendUsageSpec) DeepCopyInto(out *BackendUsageSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendUsageSpec. +func (in *BackendUsageSpec) DeepCopy() *BackendUsageSpec { + if in == nil { + return nil + } + out := new(BackendUsageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Configuration) DeepCopyInto(out *Configuration) { + *out = *in + in.Value.DeepCopyInto(&out.Value) + out.ValueFrom = in.ValueFrom +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration. +func (in *Configuration) DeepCopy() *Configuration { + if in == nil { + return nil + } + out := new(Configuration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayResponseSpec) DeepCopyInto(out *GatewayResponseSpec) { + *out = *in + if in.ErrorStatusAuthFailed != nil { + in, out := &in.ErrorStatusAuthFailed, &out.ErrorStatusAuthFailed + *out = new(int32) + **out = **in + } + if in.ErrorHeadersAuthFailed != nil { + in, out := &in.ErrorHeadersAuthFailed, &out.ErrorHeadersAuthFailed + *out = new(string) + **out = **in + } + if in.ErrorAuthFailed != nil { + in, out := &in.ErrorAuthFailed, &out.ErrorAuthFailed + *out = new(string) + **out = **in + } + if in.ErrorStatusAuthMissing != nil { + in, out := &in.ErrorStatusAuthMissing, &out.ErrorStatusAuthMissing + *out = new(int32) + **out = **in + } + if in.ErrorHeadersAuthMissing != nil { + in, out := &in.ErrorHeadersAuthMissing, &out.ErrorHeadersAuthMissing + *out = new(string) + **out = **in + } + if in.ErrorAuthMissing != nil { + in, out := &in.ErrorAuthMissing, &out.ErrorAuthMissing + *out = new(string) + **out = **in + } + if in.ErrorStatusNoMatch != nil { + in, out := &in.ErrorStatusNoMatch, &out.ErrorStatusNoMatch + *out = new(int32) + **out = **in + } + if in.ErrorHeadersNoMatch != nil { + in, out := &in.ErrorHeadersNoMatch, &out.ErrorHeadersNoMatch + *out = new(string) + **out = **in + } + if in.ErrorNoMatch != nil { + in, out := &in.ErrorNoMatch, &out.ErrorNoMatch + *out = new(string) + **out = **in + } + if in.ErrorStatusLimitsExceeded != nil { + in, out := &in.ErrorStatusLimitsExceeded, &out.ErrorStatusLimitsExceeded + *out = new(int32) + **out = **in + } + if in.ErrorHeadersLimitsExceeded != nil { + in, out := &in.ErrorHeadersLimitsExceeded, &out.ErrorHeadersLimitsExceeded + *out = new(string) + **out = **in + } + if in.ErrorLimitsExceeded != nil { + in, out := &in.ErrorLimitsExceeded, &out.ErrorLimitsExceeded + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayResponseSpec. +func (in *GatewayResponseSpec) DeepCopy() *GatewayResponseSpec { + if in == nil { + return nil + } + out := new(GatewayResponseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitSpec) DeepCopyInto(out *LimitSpec) { + *out = *in + in.MetricMethodRef.DeepCopyInto(&out.MetricMethodRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitSpec. +func (in *LimitSpec) DeepCopy() *LimitSpec { + if in == nil { + return nil + } + out := new(LimitSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MappingRuleSpec) DeepCopyInto(out *MappingRuleSpec) { + *out = *in + if in.Last != nil { + in, out := &in.Last, &out.Last + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MappingRuleSpec. +func (in *MappingRuleSpec) DeepCopy() *MappingRuleSpec { + if in == nil { + return nil + } + out := new(MappingRuleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MethodSpec) DeepCopyInto(out *MethodSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MethodSpec. +func (in *MethodSpec) DeepCopy() *MethodSpec { + if in == nil { + return nil + } + out := new(MethodSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricMethodRefSpec) DeepCopyInto(out *MetricMethodRefSpec) { + *out = *in + if in.BackendSystemName != nil { + in, out := &in.BackendSystemName, &out.BackendSystemName + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricMethodRefSpec. +func (in *MetricMethodRefSpec) DeepCopy() *MetricMethodRefSpec { + if in == nil { + return nil + } + out := new(MetricMethodRefSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricSpec) DeepCopyInto(out *MetricSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricSpec. +func (in *MetricSpec) DeepCopy() *MetricSpec { + if in == nil { + return nil + } + out := new(MetricSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCAuthenticationFlowSpec) DeepCopyInto(out *OIDCAuthenticationFlowSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCAuthenticationFlowSpec. +func (in *OIDCAuthenticationFlowSpec) DeepCopy() *OIDCAuthenticationFlowSpec { + if in == nil { + return nil + } + out := new(OIDCAuthenticationFlowSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCSpec) DeepCopyInto(out *OIDCSpec) { + *out = *in + if in.AuthenticationFlow != nil { + in, out := &in.AuthenticationFlow, &out.AuthenticationFlow + *out = new(OIDCAuthenticationFlowSpec) + **out = **in + } + if in.JwtClaimWithClientID != nil { + in, out := &in.JwtClaimWithClientID, &out.JwtClaimWithClientID + *out = new(string) + **out = **in + } + if in.JwtClaimWithClientIDType != nil { + in, out := &in.JwtClaimWithClientIDType, &out.JwtClaimWithClientIDType + *out = new(string) + **out = **in + } + if in.CredentialsLoc != nil { + in, out := &in.CredentialsLoc, &out.CredentialsLoc + *out = new(string) + **out = **in + } + if in.Security != nil { + in, out := &in.Security, &out.Security + *out = new(SecuritySpec) + (*in).DeepCopyInto(*out) + } + if in.GatewayResponse != nil { + in, out := &in.GatewayResponse, &out.GatewayResponse + *out = new(GatewayResponseSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCSpec. +func (in *OIDCSpec) DeepCopy() *OIDCSpec { + if in == nil { + return nil + } + out := new(OIDCSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyConfig) DeepCopyInto(out *PolicyConfig) { + *out = *in + in.Configuration.DeepCopyInto(&out.Configuration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyConfig. +func (in *PolicyConfig) DeepCopy() *PolicyConfig { + if in == nil { + return nil + } + out := new(PolicyConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PricingRuleSpec) DeepCopyInto(out *PricingRuleSpec) { + *out = *in + in.MetricMethodRef.DeepCopyInto(&out.MetricMethodRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PricingRuleSpec. +func (in *PricingRuleSpec) DeepCopy() *PricingRuleSpec { + if in == nil { + return nil + } + out := new(PricingRuleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Product) DeepCopyInto(out *Product) { + *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 Product. +func (in *Product) DeepCopy() *Product { + if in == nil { + return nil + } + out := new(Product) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Product) 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 *ProductDeploymentSpec) DeepCopyInto(out *ProductDeploymentSpec) { + *out = *in + if in.ApicastHosted != nil { + in, out := &in.ApicastHosted, &out.ApicastHosted + *out = new(ApicastHostedSpec) + (*in).DeepCopyInto(*out) + } + if in.ApicastSelfManaged != nil { + in, out := &in.ApicastSelfManaged, &out.ApicastSelfManaged + *out = new(ApicastSelfManagedSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProductDeploymentSpec. +func (in *ProductDeploymentSpec) DeepCopy() *ProductDeploymentSpec { + if in == nil { + return nil + } + out := new(ProductDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProductList) DeepCopyInto(out *ProductList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Product, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProductList. +func (in *ProductList) DeepCopy() *ProductList { + if in == nil { + return nil + } + out := new(ProductList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProductList) 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 *ProductSpec) DeepCopyInto(out *ProductSpec) { + *out = *in + if in.Deployment != nil { + in, out := &in.Deployment, &out.Deployment + *out = new(ProductDeploymentSpec) + (*in).DeepCopyInto(*out) + } + if in.MappingRules != nil { + in, out := &in.MappingRules, &out.MappingRules + *out = make([]MappingRuleSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.BackendUsages != nil { + in, out := &in.BackendUsages, &out.BackendUsages + *out = make(map[string]BackendUsageSpec, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make(map[string]MetricSpec, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make(map[string]MethodSpec, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ApplicationPlans != nil { + in, out := &in.ApplicationPlans, &out.ApplicationPlans + *out = make(map[string]ApplicationPlanSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.ProviderAccountRef != nil { + in, out := &in.ProviderAccountRef, &out.ProviderAccountRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.Policies != nil { + in, out := &in.Policies, &out.Policies + *out = make([]PolicyConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProductSpec. +func (in *ProductSpec) DeepCopy() *ProductSpec { + if in == nil { + return nil + } + out := new(ProductSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProductStatus) DeepCopyInto(out *ProductStatus) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(int64) + **out = **in + } + if in.State != nil { + in, out := &in.State, &out.State + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(common.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProductStatus. +func (in *ProductStatus) DeepCopy() *ProductStatus { + if in == nil { + return nil + } + out := new(ProductStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecuritySpec) DeepCopyInto(out *SecuritySpec) { + *out = *in + if in.HostHeader != nil { + in, out := &in.HostHeader, &out.HostHeader + *out = new(string) + **out = **in + } + if in.SecretToken != nil { + in, out := &in.SecretToken, &out.SecretToken + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecuritySpec. +func (in *SecuritySpec) DeepCopy() *SecuritySpec { + if in == nil { + return nil + } + out := new(SecuritySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserKeyAuthenticationSpec) DeepCopyInto(out *UserKeyAuthenticationSpec) { + *out = *in + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = new(string) + **out = **in + } + if in.CredentialsLoc != nil { + in, out := &in.CredentialsLoc, &out.CredentialsLoc + *out = new(string) + **out = **in + } + if in.Security != nil { + in, out := &in.Security, &out.Security + *out = new(SecuritySpec) + (*in).DeepCopyInto(*out) + } + if in.GatewayResponse != nil { + in, out := &in.GatewayResponse, &out.GatewayResponse + *out = new(GatewayResponseSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserKeyAuthenticationSpec. +func (in *UserKeyAuthenticationSpec) DeepCopy() *UserKeyAuthenticationSpec { + if in == nil { + return nil + } + out := new(UserKeyAuthenticationSpec) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/3scale-operator.clusterserviceversion.yaml b/bundle/manifests/3scale-operator.clusterserviceversion.yaml index 416a925b7..1b5a4982f 100644 --- a/bundle/manifests/3scale-operator.clusterserviceversion.yaml +++ b/bundle/manifests/3scale-operator.clusterserviceversion.yaml @@ -197,6 +197,16 @@ metadata: "ProductCRName": "product1-sample", "Production": true } + }, + { + "apiVersion": "capabilities.3scale.net/v1beta2", + "kind": "Product", + "metadata": { + "name": "product-sample" + }, + "spec": { + "name": "OperatedProduct 1" + } } ] capabilities: Deep Insights @@ -300,6 +310,11 @@ spec: kind: OpenAPI name: openapis.capabilities.3scale.net version: v1beta1 + - description: Product is the Schema for the products API + displayName: 3scale Product + kind: Product + name: products.capabilities.3scale.net + version: v1beta2 - description: Product is the Schema for the products API displayName: 3scale Product kind: Product @@ -471,6 +486,9 @@ spec: ports: - containerPort: 8080 name: metrics + - containerPort: 9443 + name: webhook-server + protocol: TCP resources: limits: cpu: 100m @@ -478,8 +496,17 @@ spec: requests: cpu: 100m memory: 300Mi + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true serviceAccountName: 3scale-operator terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert permissions: - rules: - apiGroups: @@ -1011,9 +1038,9 @@ spec: serviceAccountName: 3scale-operator strategy: deployment installModes: - - supported: true + - supported: false type: OwnNamespace - - supported: true + - supported: false type: SingleNamespace - supported: false type: MultiNamespace @@ -1038,3 +1065,16 @@ spec: provider: name: Red Hat version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1beta1 + - v1beta2 + containerPort: 443 + conversionCRDs: + - products.capabilities.3scale.net + deploymentName: threescale-operator-controller-manager-v2 + generateName: cproducts.capabilities.3scale.net + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert \ No newline at end of file diff --git a/bundle/manifests/capabilities.3scale.net_products.yaml b/bundle/manifests/capabilities.3scale.net_products.yaml index 3e760b061..c278bf4ad 100644 --- a/bundle/manifests/capabilities.3scale.net_products.yaml +++ b/bundle/manifests/capabilities.3scale.net_products.yaml @@ -2,12 +2,25 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + cert-manager.io/inject-ca-from: 3scale-operator-system/threescale-operator-serving-cert controller-gen.kubebuilder.io/version: v0.9.2 creationTimestamp: null labels: app: 3scale-api-management name: products.capabilities.3scale.net spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: webhook-service + namespace: 3scale-operator-system + path: /convert + port: 443 + conversionReviewVersions: + - v1beta1 + - v1beta2 group: capabilities.3scale.net names: kind: Product @@ -817,6 +830,805 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - name: v1beta2 + schema: + openAPIV3Schema: + description: Product is the Schema for the products 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: ProductSpec defines the desired state of Product + properties: + applicationPlans: + additionalProperties: + description: ApplicationPlanSpec defines the desired state of Product's Application Plan + properties: + appsRequireApproval: + description: Set whether or not applications can be created on demand or if approval is required from you before they are activated. + type: boolean + costMonth: + description: Cost per Month (USD) + pattern: ^\d+(\.\d{2})?$ + type: string + limits: + description: Limits + items: + description: LimitSpec defines the maximum value a metric can take on a contract before the user is no longer authorized to use resources. Once a limit has been passed in a given period, reject messages will be issued if the service is accessed under this contract. + properties: + metricMethodRef: + description: Metric or Method Reference + properties: + backend: + description: BackendSystemName identifies uniquely the backend Backend reference must be used by the product + type: string + systemName: + description: SystemName identifies uniquely the metric or methods + type: string + required: + - systemName + type: object + period: + description: Limit Period + enum: + - eternity + - year + - month + - week + - day + - hour + - minute + type: string + value: + description: Limit Value + type: integer + required: + - metricMethodRef + - period + - value + type: object + type: array + name: + type: string + pricingRules: + description: Pricing Rules + items: + description: PricingRuleSpec defines the cost of each operation performed on an API. Multiple pricing rules on the same metric divide up the ranges of when a pricing rule applies. + properties: + from: + description: Range From + type: integer + metricMethodRef: + description: Metric or Method Reference + properties: + backend: + description: BackendSystemName identifies uniquely the backend Backend reference must be used by the product + type: string + systemName: + description: SystemName identifies uniquely the metric or methods + type: string + required: + - systemName + type: object + pricePerUnit: + description: Price per unit (USD) + pattern: ^\d+(\.\d{2})?$ + type: string + to: + description: Range To + type: integer + required: + - from + - metricMethodRef + - pricePerUnit + - to + type: object + type: array + published: + description: Controls whether the application plan is published. If not specified it is hidden by default + type: boolean + setupFee: + description: Setup fee (USD) + pattern: ^\d+(\.\d{2})?$ + type: string + trialPeriod: + description: Trial Period (days) + minimum: 0 + type: integer + type: object + description: 'Application Plans Map: system_name -> Application Plan Spec' + type: object + backendUsages: + additionalProperties: + description: BackendUsageSpec defines the desired state of Product's Backend Usages + properties: + path: + type: string + required: + - path + type: object + description: 'Backend usage will be a map of Map: system_name -> BackendUsageSpec Having system_name as the index, the structure ensures one backend is not used multiple times.' + type: object + deployment: + description: Deployment defined 3scale product deployment mode + properties: + apicastHosted: + description: ApicastHostedSpec defines the desired state of Product Apicast Hosted + properties: + authentication: + description: AuthenticationSpec defines the desired state of Product Authentication + properties: + appKeyAppID: + description: AppKeyAppIDAuthenticationSpec defines the desired state of AppKey&AppId Authentication + properties: + appID: + description: AppID is the name of the parameter that acts of behalf of app id + type: string + appKey: + description: AppKey is the name of the parameter that acts of behalf of app key + type: string + credentials: + description: 'CredentialsLoc available options: headers: As HTTP Headers query: As query parameters (GET) or body parameters (POST/PUT/DELETE) authorization: As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + type: string + secretToken: + description: SecretToken Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + type: string + type: object + type: object + oidc: + description: OIDCSpec defines the desired configuration of OpenID Connect Authentication + properties: + authenticationFlow: + description: AuthenticationFlow specifies OAuth2.0 authorization grant type + properties: + directAccessGrantsEnabled: + type: boolean + implicitFlowEnabled: + type: boolean + serviceAccountsEnabled: + type: boolean + standardFlowEnabled: + description: OIDCIssuer is the OIDC issuer + type: boolean + required: + - directAccessGrantsEnabled + - implicitFlowEnabled + - serviceAccountsEnabled + - standardFlowEnabled + type: object + credentials: + description: 'Credentials Location available options: headers: As HTTP Headers query: As query parameters (GET) or body parameters (POST/PUT/DELETE) authorization: As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the response code when no match error + format: int32 + type: integer + type: object + issuerEndpoint: + description: Issuer is the OIDC issuer + type: string + issuerType: + description: IssuerType is the type of the OIDC issuer + enum: + - keycloak + - rest + type: string + jwtClaimWithClientID: + description: JwtClaimWithClientID is the JSON Web Token (JWT) Claim with ClientID that contains the clientID. Defaults to 'azp'. + type: string + jwtClaimWithClientIDType: + description: JwtClaimWithClientIDType sets to process the ClientID Token Claim value as a string or as a liquid template. + enum: + - plain + - liquid + type: string + security: + description: SecuritySpec defines the desired state of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + type: string + secretToken: + description: SecretToken Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + type: string + type: object + required: + - issuerEndpoint + - issuerType + type: object + userkey: + description: UserKeyAuthenticationSpec defines the desired state of User Key Authentication + properties: + authUserKey: + type: string + credentials: + description: 'Credentials Location available options: headers: As HTTP Headers query: As query parameters (GET) or body parameters (POST/PUT/DELETE) authorization: As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + type: string + secretToken: + description: SecretToken Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + type: string + type: object + type: object + type: object + type: object + apicastSelfManaged: + description: ApicastSelfManagedSpec defines the desired state of Product Apicast Self Managed + properties: + authentication: + description: AuthenticationSpec defines the desired state of Product Authentication + properties: + appKeyAppID: + description: AppKeyAppIDAuthenticationSpec defines the desired state of AppKey&AppId Authentication + properties: + appID: + description: AppID is the name of the parameter that acts of behalf of app id + type: string + appKey: + description: AppKey is the name of the parameter that acts of behalf of app key + type: string + credentials: + description: 'CredentialsLoc available options: headers: As HTTP Headers query: As query parameters (GET) or body parameters (POST/PUT/DELETE) authorization: As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + type: string + secretToken: + description: SecretToken Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + type: string + type: object + type: object + oidc: + description: OIDCSpec defines the desired configuration of OpenID Connect Authentication + properties: + authenticationFlow: + description: AuthenticationFlow specifies OAuth2.0 authorization grant type + properties: + directAccessGrantsEnabled: + type: boolean + implicitFlowEnabled: + type: boolean + serviceAccountsEnabled: + type: boolean + standardFlowEnabled: + description: OIDCIssuer is the OIDC issuer + type: boolean + required: + - directAccessGrantsEnabled + - implicitFlowEnabled + - serviceAccountsEnabled + - standardFlowEnabled + type: object + credentials: + description: 'Credentials Location available options: headers: As HTTP Headers query: As query parameters (GET) or body parameters (POST/PUT/DELETE) authorization: As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the response code when no match error + format: int32 + type: integer + type: object + issuerEndpoint: + description: Issuer is the OIDC issuer + type: string + issuerType: + description: IssuerType is the type of the OIDC issuer + enum: + - keycloak + - rest + type: string + jwtClaimWithClientID: + description: JwtClaimWithClientID is the JSON Web Token (JWT) Claim with ClientID that contains the clientID. Defaults to 'azp'. + type: string + jwtClaimWithClientIDType: + description: JwtClaimWithClientIDType sets to process the ClientID Token Claim value as a string or as a liquid template. + enum: + - plain + - liquid + type: string + security: + description: SecuritySpec defines the desired state of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + type: string + secretToken: + description: SecretToken Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + type: string + type: object + required: + - issuerEndpoint + - issuerType + type: object + userkey: + description: UserKeyAuthenticationSpec defines the desired state of User Key Authentication + properties: + authUserKey: + type: string + credentials: + description: 'Credentials Location available options: headers: As HTTP Headers query: As query parameters (GET) or body parameters (POST/PUT/DELETE) authorization: As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies the Content-Type header when authentication fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies the Content-Type header when authentication is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host. + type: string + secretToken: + description: SecretToken Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It's up to you ensure your backend only allows calls with this secret header. + type: string + type: object + type: object + type: object + productionPublicBaseURL: + pattern: ^https?:\/\/.*$ + type: string + stagingPublicBaseURL: + pattern: ^https?:\/\/.*$ + type: string + type: object + type: object + description: + description: Description is a human readable text of the product + type: string + mappingRules: + description: 'Mapping Rules Array: MappingRule Spec' + items: + description: MappingRuleSpec defines the desired state of Product's MappingRule + properties: + httpMethod: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - OPTIONS + - TRACE + - PATCH + - CONNECT + type: string + increment: + type: integer + last: + type: boolean + metricMethodRef: + type: string + pattern: + type: string + required: + - httpMethod + - increment + - metricMethodRef + - pattern + type: object + type: array + methods: + additionalProperties: + description: MethodSpec defines the desired state of Product's Method + properties: + description: + type: string + friendlyName: + type: string + required: + - friendlyName + type: object + description: 'Methods Map: system_name -> MethodSpec system_name attr is unique for all metrics AND methods In other words, if metric''s system_name is A, there is no metric or method with system_name A.' + type: object + metrics: + additionalProperties: + description: MetricSpec defines the desired state of Product's Metric + properties: + description: + type: string + friendlyName: + type: string + unit: + type: string + required: + - friendlyName + - unit + type: object + description: 'Metrics Map: system_name -> MetricSpec system_name attr is unique for all metrics AND methods In other words, if metric''s system_name is A, there is no metric or method with system_name A.' + type: object + name: + description: Name is human readable name for the product + type: string + policies: + description: Policies holds the product's policy chain + items: + description: PolicyConfig defines policy definition + properties: + configuration: + description: Configuration defines the policy configuration + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + description: ValueFrom defines the secret where the configuration can be retrieved + properties: + name: + description: name is unique within a namespace to reference a secret resource. + type: string + namespace: + description: namespace defines the space within which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + enabled: + description: Enabled defines activation state + type: boolean + name: + description: Name defines the policy unique name + type: string + version: + description: Version defines the policy version + type: string + required: + - configuration + - enabled + - name + - version + type: object + type: array + providerAccountRef: + description: ProviderAccountRef references account provider credentials + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + systemName: + description: SystemName identifies uniquely the product within the account provider Default value will be sanitized Name + type: string + required: + - name + type: object + status: + description: ProductStatus defines the observed state of Product + properties: + conditions: + description: Current state of the 3scale product. Conditions represent the latest available observations of an object's state + items: + description: "Condition represents an observation of an object's state. Conditions are an extension mechanism intended to be used when the details of an observation are not a priori known or would not apply to all instances of a given Kind. \n Conditions should be added to explicitly convey properties that users and components care about rather than requiring those properties to be inferred from other observations. Once defined, the meaning of a Condition can not be changed arbitrarily - it becomes part of the API, and has the same backwards- and forwards-compatibility concerns of any other part of the API." + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + description: ConditionReason is intended to be a one-word, CamelCase representation of the category of cause of the current status. It is intended to be used in concise output, such as one-line kubectl get output, and in summarizing occurrences of causes. + type: string + status: + type: string + type: + description: "ConditionType is the type of the condition and is typically a CamelCased word or short phrase. \n Condition types should indicate state in the \"abnormal-true\" polarity. For example, if the condition indicates when a policy is invalid, the \"is valid\" case is probably the norm, so the condition should be called \"Invalid\"." + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration reflects the generation of the most recently observed Product Spec. + format: int64 + type: integer + productId: + format: int64 + type: integer + providerAccountHost: + description: 3scale control plane host + type: string + state: + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/bundle/manifests/threescale-operator-webhook-service_v1_service.yaml b/bundle/manifests/threescale-operator-webhook-service_v1_service.yaml new file mode 100644 index 000000000..a62b3623c --- /dev/null +++ b/bundle/manifests/threescale-operator-webhook-service_v1_service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app: 3scale-api-management + name: threescale-operator-webhook-service +spec: + ports: + - port: 443 + targetPort: 9443 + selector: + app: 3scale-api-management + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/config/crd/bases/capabilities.3scale.net_products.yaml b/config/crd/bases/capabilities.3scale.net_products.yaml index 57c762a24..173c06c10 100644 --- a/config/crd/bases/capabilities.3scale.net_products.yaml +++ b/config/crd/bases/capabilities.3scale.net_products.yaml @@ -1041,6 +1041,1052 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - name: v1beta2 + schema: + openAPIV3Schema: + description: Product is the Schema for the products 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: ProductSpec defines the desired state of Product + properties: + applicationPlans: + additionalProperties: + description: ApplicationPlanSpec defines the desired state of Product's + Application Plan + properties: + appsRequireApproval: + description: Set whether or not applications can be created + on demand or if approval is required from you before they + are activated. + type: boolean + costMonth: + description: Cost per Month (USD) + pattern: ^\d+(\.\d{2})?$ + type: string + limits: + description: Limits + items: + description: LimitSpec defines the maximum value a metric + can take on a contract before the user is no longer authorized + to use resources. Once a limit has been passed in a given + period, reject messages will be issued if the service is + accessed under this contract. + properties: + metricMethodRef: + description: Metric or Method Reference + properties: + backend: + description: BackendSystemName identifies uniquely + the backend Backend reference must be used by the + product + type: string + systemName: + description: SystemName identifies uniquely the metric + or methods + type: string + required: + - systemName + type: object + period: + description: Limit Period + enum: + - eternity + - year + - month + - week + - day + - hour + - minute + type: string + value: + description: Limit Value + type: integer + required: + - metricMethodRef + - period + - value + type: object + type: array + name: + type: string + pricingRules: + description: Pricing Rules + items: + description: PricingRuleSpec defines the cost of each operation + performed on an API. Multiple pricing rules on the same + metric divide up the ranges of when a pricing rule applies. + properties: + from: + description: Range From + type: integer + metricMethodRef: + description: Metric or Method Reference + properties: + backend: + description: BackendSystemName identifies uniquely + the backend Backend reference must be used by the + product + type: string + systemName: + description: SystemName identifies uniquely the metric + or methods + type: string + required: + - systemName + type: object + pricePerUnit: + description: Price per unit (USD) + pattern: ^\d+(\.\d{2})?$ + type: string + to: + description: Range To + type: integer + required: + - from + - metricMethodRef + - pricePerUnit + - to + type: object + type: array + published: + description: Controls whether the application plan is published. + If not specified it is hidden by default + type: boolean + setupFee: + description: Setup fee (USD) + pattern: ^\d+(\.\d{2})?$ + type: string + trialPeriod: + description: Trial Period (days) + minimum: 0 + type: integer + type: object + description: 'Application Plans Map: system_name -> Application Plan + Spec' + type: object + backendUsages: + additionalProperties: + description: BackendUsageSpec defines the desired state of Product's + Backend Usages + properties: + path: + type: string + required: + - path + type: object + description: 'Backend usage will be a map of Map: system_name -> BackendUsageSpec + Having system_name as the index, the structure ensures one backend + is not used multiple times.' + type: object + deployment: + description: Deployment defined 3scale product deployment mode + properties: + apicastHosted: + description: ApicastHostedSpec defines the desired state of Product + Apicast Hosted + properties: + authentication: + description: AuthenticationSpec defines the desired state + of Product Authentication + properties: + appKeyAppID: + description: AppKeyAppIDAuthenticationSpec defines the + desired state of AppKey&AppId Authentication + properties: + appID: + description: AppID is the name of the parameter that + acts of behalf of app id + type: string + appKey: + description: AppKey is the name of the parameter that + acts of behalf of app key + type: string + credentials: + description: 'CredentialsLoc available options: headers: + As HTTP Headers query: As query parameters (GET) + or body parameters (POST/PUT/DELETE) authorization: + As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired + gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response + body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response + body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies + the Content-Type header when authentication + fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies + the Content-Type header when authentication + is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies + the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the + Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the + response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response + body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the + response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies + the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies + the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the + response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state + of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom + Host request header. This is needed if your + API backend only accepts traffic from a specific + host. + type: string + secretToken: + description: SecretToken Enables you to block + any direct developer requests to your API backend; + each 3scale API gateway call to your API backend + contains a request header called X-3scale-proxy-secret-token. + The value of this header can be set by you here. + It's up to you ensure your backend only allows + calls with this secret header. + type: string + type: object + type: object + oidc: + description: OIDCSpec defines the desired configuration + of OpenID Connect Authentication + properties: + authenticationFlow: + description: AuthenticationFlow specifies OAuth2.0 + authorization grant type + properties: + directAccessGrantsEnabled: + type: boolean + implicitFlowEnabled: + type: boolean + serviceAccountsEnabled: + type: boolean + standardFlowEnabled: + description: OIDCIssuer is the OIDC issuer + type: boolean + required: + - directAccessGrantsEnabled + - implicitFlowEnabled + - serviceAccountsEnabled + - standardFlowEnabled + type: object + credentials: + description: 'Credentials Location available options: + headers: As HTTP Headers query: As query parameters + (GET) or body parameters (POST/PUT/DELETE) authorization: + As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired + gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response + body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response + body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies + the Content-Type header when authentication + fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies + the Content-Type header when authentication + is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies + the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the + Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the + response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response + body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the + response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies + the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies + the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the + response code when no match error + format: int32 + type: integer + type: object + issuerEndpoint: + description: Issuer is the OIDC issuer + type: string + issuerType: + description: IssuerType is the type of the OIDC issuer + enum: + - keycloak + - rest + type: string + jwtClaimWithClientID: + description: JwtClaimWithClientID is the JSON Web + Token (JWT) Claim with ClientID that contains the + clientID. Defaults to 'azp'. + type: string + jwtClaimWithClientIDType: + description: JwtClaimWithClientIDType sets to process + the ClientID Token Claim value as a string or as + a liquid template. + enum: + - plain + - liquid + type: string + security: + description: SecuritySpec defines the desired state + of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom + Host request header. This is needed if your + API backend only accepts traffic from a specific + host. + type: string + secretToken: + description: SecretToken Enables you to block + any direct developer requests to your API backend; + each 3scale API gateway call to your API backend + contains a request header called X-3scale-proxy-secret-token. + The value of this header can be set by you here. + It's up to you ensure your backend only allows + calls with this secret header. + type: string + type: object + required: + - issuerEndpoint + - issuerType + type: object + userkey: + description: UserKeyAuthenticationSpec defines the desired + state of User Key Authentication + properties: + authUserKey: + type: string + credentials: + description: 'Credentials Location available options: + headers: As HTTP Headers query: As query parameters + (GET) or body parameters (POST/PUT/DELETE) authorization: + As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired + gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response + body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response + body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies + the Content-Type header when authentication + fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies + the Content-Type header when authentication + is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies + the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the + Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the + response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response + body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the + response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies + the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies + the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the + response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state + of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom + Host request header. This is needed if your + API backend only accepts traffic from a specific + host. + type: string + secretToken: + description: SecretToken Enables you to block + any direct developer requests to your API backend; + each 3scale API gateway call to your API backend + contains a request header called X-3scale-proxy-secret-token. + The value of this header can be set by you here. + It's up to you ensure your backend only allows + calls with this secret header. + type: string + type: object + type: object + type: object + type: object + apicastSelfManaged: + description: ApicastSelfManagedSpec defines the desired state + of Product Apicast Self Managed + properties: + authentication: + description: AuthenticationSpec defines the desired state + of Product Authentication + properties: + appKeyAppID: + description: AppKeyAppIDAuthenticationSpec defines the + desired state of AppKey&AppId Authentication + properties: + appID: + description: AppID is the name of the parameter that + acts of behalf of app id + type: string + appKey: + description: AppKey is the name of the parameter that + acts of behalf of app key + type: string + credentials: + description: 'CredentialsLoc available options: headers: + As HTTP Headers query: As query parameters (GET) + or body parameters (POST/PUT/DELETE) authorization: + As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired + gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response + body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response + body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies + the Content-Type header when authentication + fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies + the Content-Type header when authentication + is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies + the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the + Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the + response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response + body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the + response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies + the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies + the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the + response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state + of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom + Host request header. This is needed if your + API backend only accepts traffic from a specific + host. + type: string + secretToken: + description: SecretToken Enables you to block + any direct developer requests to your API backend; + each 3scale API gateway call to your API backend + contains a request header called X-3scale-proxy-secret-token. + The value of this header can be set by you here. + It's up to you ensure your backend only allows + calls with this secret header. + type: string + type: object + type: object + oidc: + description: OIDCSpec defines the desired configuration + of OpenID Connect Authentication + properties: + authenticationFlow: + description: AuthenticationFlow specifies OAuth2.0 + authorization grant type + properties: + directAccessGrantsEnabled: + type: boolean + implicitFlowEnabled: + type: boolean + serviceAccountsEnabled: + type: boolean + standardFlowEnabled: + description: OIDCIssuer is the OIDC issuer + type: boolean + required: + - directAccessGrantsEnabled + - implicitFlowEnabled + - serviceAccountsEnabled + - standardFlowEnabled + type: object + credentials: + description: 'Credentials Location available options: + headers: As HTTP Headers query: As query parameters + (GET) or body parameters (POST/PUT/DELETE) authorization: + As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired + gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response + body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response + body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies + the Content-Type header when authentication + fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies + the Content-Type header when authentication + is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies + the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the + Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the + response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response + body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the + response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies + the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies + the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the + response code when no match error + format: int32 + type: integer + type: object + issuerEndpoint: + description: Issuer is the OIDC issuer + type: string + issuerType: + description: IssuerType is the type of the OIDC issuer + enum: + - keycloak + - rest + type: string + jwtClaimWithClientID: + description: JwtClaimWithClientID is the JSON Web + Token (JWT) Claim with ClientID that contains the + clientID. Defaults to 'azp'. + type: string + jwtClaimWithClientIDType: + description: JwtClaimWithClientIDType sets to process + the ClientID Token Claim value as a string or as + a liquid template. + enum: + - plain + - liquid + type: string + security: + description: SecuritySpec defines the desired state + of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom + Host request header. This is needed if your + API backend only accepts traffic from a specific + host. + type: string + secretToken: + description: SecretToken Enables you to block + any direct developer requests to your API backend; + each 3scale API gateway call to your API backend + contains a request header called X-3scale-proxy-secret-token. + The value of this header can be set by you here. + It's up to you ensure your backend only allows + calls with this secret header. + type: string + type: object + required: + - issuerEndpoint + - issuerType + type: object + userkey: + description: UserKeyAuthenticationSpec defines the desired + state of User Key Authentication + properties: + authUserKey: + type: string + credentials: + description: 'Credentials Location available options: + headers: As HTTP Headers query: As query parameters + (GET) or body parameters (POST/PUT/DELETE) authorization: + As HTTP Basic Authentication' + enum: + - headers + - query + - authorization + type: string + gatewayResponse: + description: GatewayResponseSpec defines the desired + gateway response configuration + properties: + errorAuthFailed: + description: ErrorAuthFailed specifies the response + body when authentication fails + type: string + errorAuthMissing: + description: ErrorAuthMissing specifies the response + body when authentication is missing + type: string + errorHeadersAuthFailed: + description: ErrorHeadersAuthFailed specifies + the Content-Type header when authentication + fails + type: string + errorHeadersAuthMissing: + description: ErrorHeadersAuthMissing specifies + the Content-Type header when authentication + is missing + type: string + errorHeadersLimitsExceeded: + description: ErrorHeadersLimitsExceeded specifies + the Content-Type header when usage limit exceeded + type: string + errorHeadersNoMatch: + description: ErrorHeadersNoMatch specifies the + Content-Type header when no match error + type: string + errorLimitsExceeded: + description: ErrorLimitsExceeded specifies the + response body when usage limit exceeded + type: string + errorNoMatch: + description: ErrorNoMatch specifies the response + body when no match error + type: string + errorStatusAuthFailed: + description: ErrorStatusAuthFailed specifies the + response code when authentication fails + format: int32 + type: integer + errorStatusAuthMissing: + description: ErrorStatusAuthMissing specifies + the response code when authentication is missing + format: int32 + type: integer + errorStatusLimitsExceeded: + description: ErrorStatusLimitsExceeded specifies + the response code when usage limit exceeded + format: int32 + type: integer + errorStatusNoMatch: + description: ErrorStatusNoMatch specifies the + response code when no match error + format: int32 + type: integer + type: object + security: + description: SecuritySpec defines the desired state + of Authentication Security + properties: + hostHeader: + description: HostHeader Lets you define a custom + Host request header. This is needed if your + API backend only accepts traffic from a specific + host. + type: string + secretToken: + description: SecretToken Enables you to block + any direct developer requests to your API backend; + each 3scale API gateway call to your API backend + contains a request header called X-3scale-proxy-secret-token. + The value of this header can be set by you here. + It's up to you ensure your backend only allows + calls with this secret header. + type: string + type: object + type: object + type: object + productionPublicBaseURL: + pattern: ^https?:\/\/.*$ + type: string + stagingPublicBaseURL: + pattern: ^https?:\/\/.*$ + type: string + type: object + type: object + description: + description: Description is a human readable text of the product + type: string + mappingRules: + description: 'Mapping Rules Array: MappingRule Spec' + items: + description: MappingRuleSpec defines the desired state of Product's + MappingRule + properties: + httpMethod: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - OPTIONS + - TRACE + - PATCH + - CONNECT + type: string + increment: + type: integer + last: + type: boolean + metricMethodRef: + type: string + pattern: + type: string + required: + - httpMethod + - increment + - metricMethodRef + - pattern + type: object + type: array + methods: + additionalProperties: + description: MethodSpec defines the desired state of Product's Method + properties: + description: + type: string + friendlyName: + type: string + required: + - friendlyName + type: object + description: 'Methods Map: system_name -> MethodSpec system_name attr + is unique for all metrics AND methods In other words, if metric''s + system_name is A, there is no metric or method with system_name + A.' + type: object + metrics: + additionalProperties: + description: MetricSpec defines the desired state of Product's Metric + properties: + description: + type: string + friendlyName: + type: string + unit: + type: string + required: + - friendlyName + - unit + type: object + description: 'Metrics Map: system_name -> MetricSpec system_name attr + is unique for all metrics AND methods In other words, if metric''s + system_name is A, there is no metric or method with system_name + A.' + type: object + name: + description: Name is human readable name for the product + type: string + policies: + description: Policies holds the product's policy chain + items: + description: PolicyConfig defines policy definition + properties: + configuration: + description: Configuration defines the policy configuration + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + description: ValueFrom defines the secret where the configuration + can be retrieved + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which + the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + enabled: + description: Enabled defines activation state + type: boolean + name: + description: Name defines the policy unique name + type: string + version: + description: Version defines the policy version + type: string + required: + - configuration + - enabled + - name + - version + type: object + type: array + providerAccountRef: + description: ProviderAccountRef references account provider credentials + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + systemName: + description: SystemName identifies uniquely the product within the + account provider Default value will be sanitized Name + type: string + required: + - name + type: object + status: + description: ProductStatus defines the observed state of Product + properties: + conditions: + description: Current state of the 3scale product. Conditions represent + the latest available observations of an object's state + items: + description: "Condition represents an observation of an object's + state. Conditions are an extension mechanism intended to be used + when the details of an observation are not a priori known or would + not apply to all instances of a given Kind. \n Conditions should + be added to explicitly convey properties that users and components + care about rather than requiring those properties to be inferred + from other observations. Once defined, the meaning of a Condition + can not be changed arbitrarily - it becomes part of the API, and + has the same backwards- and forwards-compatibility concerns of + any other part of the API." + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + description: ConditionReason is intended to be a one-word, CamelCase + representation of the category of cause of the current status. + It is intended to be used in concise output, such as one-line + kubectl get output, and in summarizing occurrences of causes. + type: string + status: + type: string + type: + description: "ConditionType is the type of the condition and + is typically a CamelCased word or short phrase. \n Condition + types should indicate state in the \"abnormal-true\" polarity. + For example, if the condition indicates when a policy is invalid, + the \"is valid\" case is probably the norm, so the condition + should be called \"Invalid\"." + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration reflects the generation of the most + recently observed Product Spec. + format: int64 + type: integer + productId: + format: int64 + type: integer + providerAccountHost: + description: 3scale control plane host + type: string + state: + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 9067775c0..428c41652 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -25,14 +25,7 @@ patchesStrategicMerge: #- patches/webhook_in_apimanagerrestores.yaml #- patches/webhook_in_tenants.yaml #- patches/webhook_in_backends.yaml -#- patches/webhook_in_products.yaml -#- patches/webhook_in_openapis.yaml -#- patches/webhook_in_activedocs.yaml -#- patches/webhook_in_developeraccounts.yaml -#- patches/webhook_in_developerusers.yaml -#- patches/webhook_in_custompolicydefinitions.yaml -#- patches/webhook_in_proxyconfigpromotes.yaml -#- patches/webhook_in_applications.yaml +- patches/webhook_in_products.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -42,7 +35,7 @@ patchesStrategicMerge: #- patches/cainjection_in_apimanagerrestores.yaml #- patches/cainjection_in_tenants.yaml #- patches/cainjection_in_backends.yaml -#- patches/cainjection_in_products.yaml +- patches/cainjection_in_products.yaml #- patches/cainjection_in_openapis.yaml #- patches/cainjection_in_activedocs.yaml #- patches/cainjection_in_developeraccounts.yaml diff --git a/config/crd/patches/webhook_in_products.yaml b/config/crd/patches/webhook_in_products.yaml index 5ec4ac8df..2ae66e61a 100644 --- a/config/crd/patches/webhook_in_products.yaml +++ b/config/crd/patches/webhook_in_products.yaml @@ -7,11 +7,11 @@ metadata: spec: conversion: strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + port: 443 + conversionReviewVersions: ["v1beta1", "v1beta2"] diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 6c8ebe3b9..c50eb92c2 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -18,9 +18,9 @@ bases: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. - ../prometheus @@ -32,7 +32,7 @@ patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- manager_webhook_patch.yaml +- manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. @@ -43,29 +43,29 @@ patchesStrategicMerge: # the following config is for teaching kustomize how to do var substitution vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1alpha2 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldref: -# fieldpath: metadata.namespace -#- name: CERTIFICATE_NAME -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1alpha2 -# name: serving-cert # this name should match the one in certificate.yaml -#- name: SERVICE_NAMESPACE # namespace of the service -# objref: -# kind: Service -# version: v1 -# name: webhook-service -# fieldref: -# fieldpath: metadata.namespace -#- name: SERVICE_NAME -# objref: -# kind: Service -# version: v1 -# name: webhook-service +- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR + objref: + kind: Certificate + group: cert-manager.io + version: v1alpha2 + name: serving-cert # this name should match the one in certificate.yaml + fieldref: + fieldpath: metadata.namespace +- name: CERTIFICATE_NAME + objref: + kind: Certificate + group: cert-manager.io + version: v1alpha2 + name: serving-cert # this name should match the one in certificate.yaml +- name: SERVICE_NAMESPACE # namespace of the service + objref: + kind: Service + version: v1 + name: webhook-service + fieldref: + fieldpath: metadata.namespace +- name: SERVICE_NAME + objref: + kind: Service + version: v1 + name: webhook-service diff --git a/config/manifests/bases/3scale-operator.clusterserviceversion.yaml b/config/manifests/bases/3scale-operator.clusterserviceversion.yaml index 4533a416d..e9e1e928d 100644 --- a/config/manifests/bases/3scale-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/3scale-operator.clusterserviceversion.yaml @@ -20,7 +20,7 @@ metadata: operatorframework.io/arch.amd64: supported operatorframework.io/arch.ppc64le: supported operatorframework.io/arch.s390x: supported - name: 3scale-operator.vX.Y.Z + name: 3scale-operator.v0.0.0 namespace: placeholder spec: apiservicedefinitions: {} @@ -31,26 +31,6 @@ spec: kind: APIManagerRestore name: apimanagerrestores.apps.3scale.net version: v1alpha1 - - description: Tenant is the Schema for the tenants API - displayName: Tenant - kind: Tenant - name: tenants.capabilities.3scale.net - version: v1alpha1 - - description: Backend is the Schema for the backends API - displayName: 3scale Backend - kind: Backend - name: backends.capabilities.3scale.net - version: v1beta1 - - description: Product is the Schema for the products API - displayName: 3scale Product - kind: Product - name: products.capabilities.3scale.net - version: v1beta1 - - description: OpenAPI is the Schema for the openapis API - displayName: Open API - kind: OpenAPI - name: openapis.capabilities.3scale.net - version: v1beta1 - description: APIManager is the Schema for the apimanagers API displayName: APIManager kind: APIManager @@ -94,6 +74,16 @@ spec: kind: ActiveDoc name: activedocs.capabilities.3scale.net version: v1beta1 + - description: Application is the Schema for the applications API + displayName: Application + kind: Application + name: applications.capabilities.3scale.net + version: v1beta1 + - description: Backend is the Schema for the backends API + displayName: 3scale Backend + kind: Backend + name: backends.capabilities.3scale.net + version: v1beta1 - description: CustomPolicyDefinition is the Schema for the custompolicydefinitions API displayName: Custom Policy Definition kind: CustomPolicyDefinition @@ -109,16 +99,31 @@ spec: kind: DeveloperUser name: developerusers.capabilities.3scale.net version: v1beta1 + - description: OpenAPI is the Schema for the openapis API + displayName: Open API + kind: OpenAPI + name: openapis.capabilities.3scale.net + version: v1beta1 + - description: Product is the Schema for the products API + displayName: 3scale Product + kind: Product + name: products.capabilities.3scale.net + version: v1beta2 + - description: Product is the Schema for the products API + displayName: 3scale Product + kind: Product + name: products.capabilities.3scale.net + version: v1beta1 - description: ProxyConfigPromote is the Schema for the proxyconfigpromotes API displayName: Proxy Config Promote kind: ProxyConfigPromote name: proxyconfigpromotes.capabilities.3scale.net version: v1beta1 - - description: Application is the Schema for the applications API - displayName: Application - kind: Application - name: applications.capabilities.3scale.net - version: v1beta1 + - description: Tenant is the Schema for the tenants API + displayName: Tenant + kind: Tenant + name: tenants.capabilities.3scale.net + version: v1alpha1 description: | The 3scale Operator creates and maintains the Red Hat 3scale API Management on [OpenShift](https://www.openshift.com/) in various deployment configurations. @@ -158,9 +163,9 @@ spec: deployments: null strategy: "" installModes: - - supported: true + - supported: false type: OwnNamespace - - supported: true + - supported: false type: SingleNamespace - supported: false type: MultiNamespace @@ -185,3 +190,16 @@ spec: provider: name: Red Hat version: 0.0.0 + webhookdefinitions: + - admissionReviewVersions: + - v1beta1 + - v1beta2 + containerPort: 443 + conversionCRDs: + - products.capabilities.3scale.net + deploymentName: threescale-operator-controller-manager-v2 + generateName: cproducts.capabilities.3scale.net + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert diff --git a/config/samples/capabilities_v1beta2_product.yaml b/config/samples/capabilities_v1beta2_product.yaml new file mode 100644 index 000000000..9fbab26e2 --- /dev/null +++ b/config/samples/capabilities_v1beta2_product.yaml @@ -0,0 +1,6 @@ +apiVersion: capabilities.3scale.net/v1beta2 +kind: Product +metadata: + name: product-sample +spec: + name: "OperatedProduct 1" diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index e6f7227a6..f13271dc5 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -13,4 +13,5 @@ resources: - capabilities_v1beta1_custompolicydefinition.yaml - capabilities_v1beta1_proxyconfigpromote.yaml - capabilities_v1beta1_application.yaml +- capabilities_v1beta2_product.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/controllers/capabilities/application_controller.go b/controllers/capabilities/application_controller.go index 1387f1dc6..023bdffe1 100644 --- a/controllers/capabilities/application_controller.go +++ b/controllers/capabilities/application_controller.go @@ -20,6 +20,8 @@ import ( "context" "encoding/json" "fmt" + + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" @@ -180,7 +182,7 @@ func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) func (r *ApplicationReconciler) applicationReconciler(applicationResource *capabilitiesv1beta1.Application, req ctrl.Request, threescaleAPIClient *threescaleapi.ThreeScaleClient, providerAccountAdminURLStr string, accountResource *capabilitiesv1beta1.DeveloperAccount) (*ApplicationStatusReconciler, error) { // get product - productResource := &capabilitiesv1beta1.Product{} + productResource := &capabilitiesv1beta2.Product{} projectMeta := types.NamespacedName{ Name: applicationResource.Spec.ProductCR.Name, Namespace: req.Namespace, @@ -241,7 +243,7 @@ func (r *ApplicationReconciler) removeApplicationFrom3scale(application *capabil return nil } -func (r *ApplicationReconciler) checkExternalResources(applicationResource *capabilitiesv1beta1.Application, accountResource *capabilitiesv1beta1.DeveloperAccount, productResource *capabilitiesv1beta1.Product) error { +func (r *ApplicationReconciler) checkExternalResources(applicationResource *capabilitiesv1beta1.Application, accountResource *capabilitiesv1beta1.DeveloperAccount, productResource *capabilitiesv1beta2.Product) error { errors := field.ErrorList{} specFldPath := field.NewPath("spec") @@ -257,7 +259,7 @@ func (r *ApplicationReconciler) checkExternalResources(applicationResource *capa if accountResource.Status.Conditions.IsTrueFor(capabilitiesv1beta1.DeveloperAccountInvalidConditionType) { errors = append(errors, field.Invalid(accountFldPath, applicationResource.Spec.AccountCR, "account CR is in an invalid state")) } - if productResource.Status.Conditions.IsTrueFor(capabilitiesv1beta1.ProductInvalidConditionType) { + if productResource.Status.Conditions.IsTrueFor(capabilitiesv1beta2.ProductInvalidConditionType) { errors = append(errors, field.Invalid(productFldPath, applicationResource.Spec.ProductCR, "product CR is in an invalid state")) } diff --git a/controllers/capabilities/application_plan_reconciler.go b/controllers/capabilities/application_plan_reconciler.go index 59fbe34f6..a3e69991b 100644 --- a/controllers/capabilities/application_plan_reconciler.go +++ b/controllers/capabilities/application_plan_reconciler.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" @@ -29,7 +29,7 @@ type pricingRuleKey struct { type applicationPlanReconciler struct { *reconcilers.BaseReconciler systemName string - resource capabilitiesv1beta1.ApplicationPlanSpec + resource capabilitiesv1beta2.ApplicationPlanSpec productEntity *controllerhelper.ProductEntity backendRemoteIndex *controllerhelper.BackendAPIRemoteIndex planEntity *controllerhelper.ApplicationPlanEntity @@ -39,7 +39,7 @@ type applicationPlanReconciler struct { func newApplicationPlanReconciler(b *reconcilers.BaseReconciler, systemName string, - resource capabilitiesv1beta1.ApplicationPlanSpec, + resource capabilitiesv1beta2.ApplicationPlanSpec, threescaleAPIClient *threescaleapi.ThreeScaleClient, productEntity *controllerhelper.ProductEntity, backendRemoteIndex *controllerhelper.BackendAPIRemoteIndex, @@ -236,7 +236,7 @@ func (a *applicationPlanReconciler) syncPricingRules(_ interface{}) error { func (a *applicationPlanReconciler) computeUnDesiredLimits( existingList []threescaleapi.ApplicationPlanLimit, - desiredList []capabilitiesv1beta1.LimitSpec) ([]threescaleapi.ApplicationPlanLimit, error) { + desiredList []capabilitiesv1beta2.LimitSpec) ([]threescaleapi.ApplicationPlanLimit, error) { target := map[limitKey]bool{} for _, desired := range desiredList { @@ -271,8 +271,8 @@ func (a *applicationPlanReconciler) computeUnDesiredLimits( } func (a *applicationPlanReconciler) computeDesiredLimits( - desiredList []capabilitiesv1beta1.LimitSpec, - existingList []threescaleapi.ApplicationPlanLimit) ([]capabilitiesv1beta1.LimitSpec, error) { + desiredList []capabilitiesv1beta2.LimitSpec, + existingList []threescaleapi.ApplicationPlanLimit) ([]capabilitiesv1beta2.LimitSpec, error) { target := map[limitKey]bool{} for _, existing := range existingList { @@ -285,7 +285,7 @@ func (a *applicationPlanReconciler) computeDesiredLimits( target[existingKey] = true } - result := make([]capabilitiesv1beta1.LimitSpec, 0) + result := make([]capabilitiesv1beta2.LimitSpec, 0) for _, desired := range desiredList { metricID, err := a.findID(desired.MetricMethodRef) if err != nil { @@ -305,7 +305,7 @@ func (a *applicationPlanReconciler) computeDesiredLimits( return result, nil } -func (a *applicationPlanReconciler) findID(ref capabilitiesv1beta1.MetricMethodRefSpec) (int64, error) { +func (a *applicationPlanReconciler) findID(ref capabilitiesv1beta2.MetricMethodRefSpec) (int64, error) { var ( metricID int64 err error @@ -330,7 +330,7 @@ func (a *applicationPlanReconciler) findID(ref capabilitiesv1beta1.MetricMethodR func (a *applicationPlanReconciler) computeUnDesiredPricingRules( existingList []threescaleapi.ApplicationPlanPricingRule, - desiredList []capabilitiesv1beta1.PricingRuleSpec) ([]threescaleapi.ApplicationPlanPricingRule, error) { + desiredList []capabilitiesv1beta2.PricingRuleSpec) ([]threescaleapi.ApplicationPlanPricingRule, error) { target := map[pricingRuleKey]bool{} for _, desired := range desiredList { @@ -367,8 +367,8 @@ func (a *applicationPlanReconciler) computeUnDesiredPricingRules( } func (a *applicationPlanReconciler) computeDesiredPricingRules( - desiredList []capabilitiesv1beta1.PricingRuleSpec, - existingList []threescaleapi.ApplicationPlanPricingRule) ([]capabilitiesv1beta1.PricingRuleSpec, error) { + desiredList []capabilitiesv1beta2.PricingRuleSpec, + existingList []threescaleapi.ApplicationPlanPricingRule) ([]capabilitiesv1beta2.PricingRuleSpec, error) { target := map[pricingRuleKey]bool{} for _, existing := range existingList { @@ -382,7 +382,7 @@ func (a *applicationPlanReconciler) computeDesiredPricingRules( target[existingKey] = true } - result := make([]capabilitiesv1beta1.PricingRuleSpec, 0) + result := make([]capabilitiesv1beta2.PricingRuleSpec, 0) for _, desired := range desiredList { metricID, err := a.findID(desired.MetricMethodRef) if err != nil { diff --git a/controllers/capabilities/application_status_reconciler_test.go b/controllers/capabilities/application_status_reconciler_test.go index 31f9cc49d..bf04e4a52 100644 --- a/controllers/capabilities/application_status_reconciler_test.go +++ b/controllers/capabilities/application_status_reconciler_test.go @@ -2,8 +2,13 @@ package controllers import ( "context" + "reflect" + "testing" + "time" + appsv1alpha1 "github.com/3scale/3scale-operator/apis/apps/v1alpha1" capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" @@ -15,12 +20,9 @@ import ( fakeclientset "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/record" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client/fake" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "testing" - "time" ) func getApplicationCR() (CR *capabilitiesv1beta1.Application) { @@ -189,27 +191,27 @@ func unknowAccountApplicationCR() (CR *capabilitiesv1beta1.Application) { } return CR } -func getApplicationProductCR() (CR *capabilitiesv1beta1.Product) { +func getApplicationProductCR() (CR *capabilitiesv1beta2.Product) { // used for string pointer test := "test" - CR = &capabilitiesv1beta1.Product{ + CR = &capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "test", }, - Spec: capabilitiesv1beta1.ProductSpec{ + Spec: capabilitiesv1beta2.ProductSpec{ Name: "test", SystemName: "test", Description: "test", - ApplicationPlans: map[string]capabilitiesv1beta1.ApplicationPlanSpec{ + ApplicationPlans: map[string]capabilitiesv1beta2.ApplicationPlanSpec{ "test": { Name: &test, - Limits: []capabilitiesv1beta1.LimitSpec{ + Limits: []capabilitiesv1beta2.LimitSpec{ { Period: "month", Value: 300, - MetricMethodRef: capabilitiesv1beta1.MetricMethodRefSpec{ + MetricMethodRef: capabilitiesv1beta2.MetricMethodRefSpec{ SystemName: "test", BackendSystemName: &test, }, @@ -219,7 +221,7 @@ func getApplicationProductCR() (CR *capabilitiesv1beta1.Product) { }, }, }, - Status: capabilitiesv1beta1.ProductStatus{ + Status: capabilitiesv1beta2.ProductStatus{ ID: create(3), ProviderAccountHost: "some string", ObservedGeneration: 1, diff --git a/controllers/capabilities/application_threescale_reconciler.go b/controllers/capabilities/application_threescale_reconciler.go index dbb023067..eb95fa2f5 100644 --- a/controllers/capabilities/application_threescale_reconciler.go +++ b/controllers/capabilities/application_threescale_reconciler.go @@ -2,13 +2,15 @@ package controllers import ( "fmt" + "strconv" + capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" threescaleapi "github.com/3scale/3scale-porta-go-client/client" "github.com/go-logr/logr" - "strconv" ) type ApplicationThreescaleReconciler struct { @@ -16,12 +18,12 @@ type ApplicationThreescaleReconciler struct { applicationResource *capabilitiesv1beta1.Application applicationEntity *controllerhelper.ApplicationEntity accountResource *capabilitiesv1beta1.DeveloperAccount - productResource *capabilitiesv1beta1.Product + productResource *capabilitiesv1beta2.Product threescaleAPIClient *threescaleapi.ThreeScaleClient logger logr.Logger } -func NewApplicationReconciler(b *reconcilers.BaseReconciler, applicationResource *capabilitiesv1beta1.Application, accountResource *capabilitiesv1beta1.DeveloperAccount, productResource *capabilitiesv1beta1.Product, threescaleAPIClient *threescaleapi.ThreeScaleClient) *ApplicationThreescaleReconciler { +func NewApplicationReconciler(b *reconcilers.BaseReconciler, applicationResource *capabilitiesv1beta1.Application, accountResource *capabilitiesv1beta1.DeveloperAccount, productResource *capabilitiesv1beta2.Product, threescaleAPIClient *threescaleapi.ThreeScaleClient) *ApplicationThreescaleReconciler { return &ApplicationThreescaleReconciler{ BaseReconciler: b, applicationResource: applicationResource, diff --git a/controllers/capabilities/applications_test.go b/controllers/capabilities/applications_test.go index de9428649..078fa3754 100644 --- a/controllers/capabilities/applications_test.go +++ b/controllers/capabilities/applications_test.go @@ -1,12 +1,14 @@ package controllers import ( + "testing" + capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" threescaleapi "github.com/3scale/3scale-porta-go-client/client" "github.com/go-logr/logr" - "testing" ) func getApplicationEntity() *controllerhelper.ApplicationEntity { @@ -94,7 +96,7 @@ func TestApplicationThreescaleReconciler_syncApplication(t1 *testing.T) { applicationResource *capabilitiesv1beta1.Application applicationEntity *controllerhelper.ApplicationEntity accountResource *capabilitiesv1beta1.DeveloperAccount - productResource *capabilitiesv1beta1.Product + productResource *capabilitiesv1beta2.Product threescaleAPIClient *threescaleapi.ThreeScaleClient logger logr.Logger } diff --git a/controllers/capabilities/backend_controller.go b/controllers/capabilities/backend_controller.go index 17d5d669e..416250496 100644 --- a/controllers/capabilities/backend_controller.go +++ b/controllers/capabilities/backend_controller.go @@ -22,6 +22,7 @@ import ( "fmt" "time" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" threescaleapi "github.com/3scale/3scale-porta-go-client/client" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -245,7 +246,7 @@ func (r *BackendReconciler) removeBackendReferencesFromProducts(backend *capabil opts := client.ListOptions{ Namespace: backend.Namespace, } - productCRsList := &capabilitiesv1beta1.ProductList{} + productCRsList := &capabilitiesv1beta2.ProductList{} err := r.Client().List(context.TODO(), productCRsList, &opts) if err != nil { return ctrl.Result{}, err @@ -312,10 +313,10 @@ func (r *BackendReconciler) removeBackendFrom3scale(backend *capabilitiesv1beta1 return nil } -func (r *BackendReconciler) fetchTenantProductCRs(productsCRsList *capabilitiesv1beta1.ProductList, backendResource *capabilitiesv1beta1.Backend) ([]capabilitiesv1beta1.Product, error) { +func (r *BackendReconciler) fetchTenantProductCRs(productsCRsList *capabilitiesv1beta2.ProductList, backendResource *capabilitiesv1beta1.Backend) ([]capabilitiesv1beta2.Product, error) { logger := r.Logger().WithValues("backend", client.ObjectKey{Name: backendResource.Name, Namespace: backendResource.Namespace}) - var productsList []capabilitiesv1beta1.Product + var productsList []capabilitiesv1beta2.Product backendProviderAccount, err := controllerhelper.LookupProviderAccount(r.Client(), backendResource.Namespace, backendResource.Spec.ProviderAccountRef, logger) if apierrors.IsNotFound(err) { diff --git a/controllers/capabilities/backend_threescale_reconciler.go b/controllers/capabilities/backend_threescale_reconciler.go index ed8e8e8eb..5db042256 100644 --- a/controllers/capabilities/backend_threescale_reconciler.go +++ b/controllers/capabilities/backend_threescale_reconciler.go @@ -6,6 +6,7 @@ import ( "strconv" capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" @@ -169,7 +170,7 @@ func (t *BackendThreescaleReconciler) syncMethods(_ interface{}) error { // Create not existing and desired // desiredNewKeys := helper.ArrayStringDifference(desiredKeys, existingKeys) - desiredNewMap := map[string]capabilitiesv1beta1.MethodSpec{} + desiredNewMap := map[string]capabilitiesv1beta2.MethodSpec{} for _, systemName := range desiredNewKeys { // key is expected to exist // desiredNewKeys is a subset of the Spec.Method map key set @@ -183,7 +184,7 @@ func (t *BackendThreescaleReconciler) syncMethods(_ interface{}) error { return nil } -func (t *BackendThreescaleReconciler) createNewMethods(desiredNewMap map[string]capabilitiesv1beta1.MethodSpec) error { +func (t *BackendThreescaleReconciler) createNewMethods(desiredNewMap map[string]capabilitiesv1beta2.MethodSpec) error { for systemName, method := range desiredNewMap { params := threescaleapi.Params{ "friendly_name": method.Name, @@ -218,7 +219,7 @@ func (t *BackendThreescaleReconciler) deleteExternalMetricReferences(notDesiredM } // filter products referencing current backend resource - linkedProductList := make([]capabilitiesv1beta1.Product, 0) + linkedProductList := make([]capabilitiesv1beta2.Product, 0) for _, product := range productList { if _, ok := product.Spec.BackendUsages[t.backendResource.Spec.SystemName]; ok { linkedProductList = append(linkedProductList, product) @@ -238,7 +239,7 @@ func (t *BackendThreescaleReconciler) deleteExternalMetricReferences(notDesiredM } // valid for metrics and methods as long as 3scale ensures system_names are unique among methods and metrics -func (t *BackendThreescaleReconciler) deleteExternalMetricReferencesOnProduct(notDesiredMetrics []string, productRef capabilitiesv1beta1.Product) error { +func (t *BackendThreescaleReconciler) deleteExternalMetricReferencesOnProduct(notDesiredMetrics []string, productRef capabilitiesv1beta2.Product) error { productUpdated := false product := productRef.DeepCopy() @@ -246,7 +247,7 @@ func (t *BackendThreescaleReconciler) deleteExternalMetricReferencesOnProduct(no planSpecUpdated := false // Check limits with external references to the current backend - newLimits := make([]capabilitiesv1beta1.LimitSpec, 0) + newLimits := make([]capabilitiesv1beta2.LimitSpec, 0) for limitIdx, limitSpec := range planSpec.Limits { // Check if the limit belongs to the current backend // Check if the limit is marked for deletion in notDesiredMap @@ -263,7 +264,7 @@ func (t *BackendThreescaleReconciler) deleteExternalMetricReferencesOnProduct(no } // Check pricingRules with external references to the current backend - newRules := make([]capabilitiesv1beta1.PricingRuleSpec, 0) + newRules := make([]capabilitiesv1beta2.PricingRuleSpec, 0) for ruleIdx, ruleSpec := range planSpec.PricingRules { // Check if the current rule belongs to the current backend if ruleSpec.MetricMethodRef.BackendSystemName == nil || @@ -379,7 +380,7 @@ func (t *BackendThreescaleReconciler) syncMetrics(_ interface{}) error { // desiredNewKeys := helper.ArrayStringDifference(desiredKeys, existingKeys) - desiredNewMap := map[string]capabilitiesv1beta1.MetricSpec{} + desiredNewMap := map[string]capabilitiesv1beta2.MetricSpec{} for _, systemName := range desiredNewKeys { // key is expected to exist // desiredNewKeys is a subset of the Spec.Metrics map key set @@ -393,7 +394,7 @@ func (t *BackendThreescaleReconciler) syncMetrics(_ interface{}) error { return nil } -func (t *BackendThreescaleReconciler) createNewMetrics(desiredNewMap map[string]capabilitiesv1beta1.MetricSpec) error { +func (t *BackendThreescaleReconciler) createNewMetrics(desiredNewMap map[string]capabilitiesv1beta2.MetricSpec) error { for systemName, metric := range desiredNewMap { params := threescaleapi.Params{ "friendly_name": metric.Name, diff --git a/controllers/capabilities/backend_usages.go b/controllers/capabilities/backend_usages.go index 2524cbff4..ba5cbbf71 100644 --- a/controllers/capabilities/backend_usages.go +++ b/controllers/capabilities/backend_usages.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" @@ -13,12 +13,12 @@ import ( type backendUsageData struct { item threescaleapi.BackendAPIUsageItem - spec capabilitiesv1beta1.BackendUsageSpec + spec capabilitiesv1beta2.BackendUsageSpec } type newBackendUsageData struct { item *controllerhelper.BackendAPIEntity - spec capabilitiesv1beta1.BackendUsageSpec + spec capabilitiesv1beta2.BackendUsageSpec } func (t *ProductThreescaleReconciler) syncBackendUsage(_ interface{}) error { diff --git a/controllers/capabilities/mapping_rules.go b/controllers/capabilities/mapping_rules.go index 763d5fd23..dcbb7c2fa 100644 --- a/controllers/capabilities/mapping_rules.go +++ b/controllers/capabilities/mapping_rules.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/helper" threescaleapi "github.com/3scale/3scale-porta-go-client/client" @@ -13,7 +13,7 @@ import ( func (t *ProductThreescaleReconciler) syncMappingRules(_ interface{}) error { desiredKeys := make([]string, 0, len(t.resource.Spec.MappingRules)) - desiredMap := map[string]capabilitiesv1beta1.MappingRuleSpec{} + desiredMap := map[string]capabilitiesv1beta2.MappingRuleSpec{} for _, spec := range t.resource.Spec.MappingRules { key := fmt.Sprintf("%s:%s", spec.HTTPMethod, spec.Pattern) desiredKeys = append(desiredKeys, key) @@ -127,7 +127,7 @@ func (t *ProductThreescaleReconciler) getExistingMappingRules() (map[string]thre return existingMap, nil } -func (t *ProductThreescaleReconciler) reconcileMappingRuleWithPosition(desired capabilitiesv1beta1.MappingRuleSpec, desiredPosition int, existing threescaleapi.MappingRuleItem) error { +func (t *ProductThreescaleReconciler) reconcileMappingRuleWithPosition(desired capabilitiesv1beta2.MappingRuleSpec, desiredPosition int, existing threescaleapi.MappingRuleItem) error { params := threescaleapi.Params{} // @@ -182,7 +182,7 @@ func (t *ProductThreescaleReconciler) reconcileMappingRuleWithPosition(desired c return nil } -func (t *ProductThreescaleReconciler) createNewMappingRuleWithPosition(desired capabilitiesv1beta1.MappingRuleSpec, desiredPosition int) error { +func (t *ProductThreescaleReconciler) createNewMappingRuleWithPosition(desired capabilitiesv1beta2.MappingRuleSpec, desiredPosition int) error { metricID, err := t.productEntity.FindMethodMetricIDBySystemName(desired.MetricMethodRef) if err != nil { return fmt.Errorf("Error creating product [%s] mappingrule: %w", t.resource.Spec.SystemName, err) diff --git a/controllers/capabilities/methods.go b/controllers/capabilities/methods.go index d005c6d89..2ac956a4e 100644 --- a/controllers/capabilities/methods.go +++ b/controllers/capabilities/methods.go @@ -3,7 +3,7 @@ package controllers import ( "fmt" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/helper" threescaleapi "github.com/3scale/3scale-porta-go-client/client" @@ -11,7 +11,7 @@ import ( type methodData struct { item threescaleapi.MethodItem - spec capabilitiesv1beta1.MethodSpec + spec capabilitiesv1beta2.MethodSpec } func (t *ProductThreescaleReconciler) syncMethods(_ interface{}) error { @@ -72,7 +72,7 @@ func (t *ProductThreescaleReconciler) syncMethods(_ interface{}) error { // desiredNewKeys := helper.ArrayStringDifference(desiredKeys, existingKeys) t.logger.V(1).Info("syncMethods", "desiredNewKeys", desiredNewKeys) - desiredNewMap := map[string]capabilitiesv1beta1.MethodSpec{} + desiredNewMap := map[string]capabilitiesv1beta2.MethodSpec{} for _, systemName := range desiredNewKeys { // key is expected to exist // desiredNewKeys is a subset of the Spec.Method map key set @@ -86,7 +86,7 @@ func (t *ProductThreescaleReconciler) syncMethods(_ interface{}) error { return nil } -func (t *ProductThreescaleReconciler) createNewMethods(desiredNewMap map[string]capabilitiesv1beta1.MethodSpec) error { +func (t *ProductThreescaleReconciler) createNewMethods(desiredNewMap map[string]capabilitiesv1beta2.MethodSpec) error { for systemName, method := range desiredNewMap { params := threescaleapi.Params{ "friendly_name": method.Name, diff --git a/controllers/capabilities/metrics.go b/controllers/capabilities/metrics.go index a26681c6a..7f37b2597 100644 --- a/controllers/capabilities/metrics.go +++ b/controllers/capabilities/metrics.go @@ -3,7 +3,7 @@ package controllers import ( "fmt" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/helper" threescaleapi "github.com/3scale/3scale-porta-go-client/client" @@ -11,7 +11,7 @@ import ( type metricData struct { item threescaleapi.MetricItem - spec capabilitiesv1beta1.MetricSpec + spec capabilitiesv1beta2.MetricSpec } func (t *ProductThreescaleReconciler) syncMetrics(_ interface{}) error { @@ -75,7 +75,7 @@ func (t *ProductThreescaleReconciler) syncMetrics(_ interface{}) error { desiredNewKeys := helper.ArrayStringDifference(desiredKeys, existingKeys) t.logger.V(1).Info("syncMetrics", "desiredNewKeys", desiredNewKeys) - desiredNewMap := map[string]capabilitiesv1beta1.MetricSpec{} + desiredNewMap := map[string]capabilitiesv1beta2.MetricSpec{} for _, systemName := range desiredNewKeys { // key is expected to exist // desiredNewKeys is a subset of the Spec.Metrics map key set @@ -125,7 +125,7 @@ func (t *ProductThreescaleReconciler) reconcileMatchedMetrics(matchedMap map[str return nil } -func (t *ProductThreescaleReconciler) createNewMetrics(desiredNewMap map[string]capabilitiesv1beta1.MetricSpec) error { +func (t *ProductThreescaleReconciler) createNewMetrics(desiredNewMap map[string]capabilitiesv1beta2.MetricSpec) error { for systemName, metric := range desiredNewMap { params := threescaleapi.Params{ "friendly_name": metric.Name, diff --git a/controllers/capabilities/openapi_controller.go b/controllers/capabilities/openapi_controller.go index 0fef8aeeb..7bce319e3 100644 --- a/controllers/capabilities/openapi_controller.go +++ b/controllers/capabilities/openapi_controller.go @@ -22,6 +22,7 @@ import ( "fmt" "net/url" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -205,7 +206,7 @@ func (r *OpenAPIReconciler) checkProductSynced(resource *capabilitiesv1beta1.Ope } // Fetch the Product instance - product := &capabilitiesv1beta1.Product{} + product := &capabilitiesv1beta2.Product{} objectKey := client.ObjectKey{Name: resource.Status.ProductResourceName.Name, Namespace: resource.Namespace} err := r.Client().Get(r.Context(), objectKey, product) if err != nil { @@ -216,7 +217,7 @@ func (r *OpenAPIReconciler) checkProductSynced(resource *capabilitiesv1beta1.Ope return false, err } - return product.Status.Conditions.IsTrueFor(capabilitiesv1beta1.ProductSyncedConditionType), nil + return product.Status.Conditions.IsTrueFor(capabilitiesv1beta2.ProductSyncedConditionType), nil } func (r *OpenAPIReconciler) readOpenAPI(resource *capabilitiesv1beta1.OpenAPI) (*openapi3.T, error) { diff --git a/controllers/capabilities/openapi_product_reconciler.go b/controllers/capabilities/openapi_product_reconciler.go index c0f73dfb4..940467085 100644 --- a/controllers/capabilities/openapi_product_reconciler.go +++ b/controllers/capabilities/openapi_product_reconciler.go @@ -9,6 +9,7 @@ import ( "strings" capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" @@ -54,7 +55,7 @@ func (p *OpenAPIProductReconciler) Logger() logr.Logger { return p.logger } -func (p *OpenAPIProductReconciler) Reconcile() (*capabilitiesv1beta1.Product, error) { +func (p *OpenAPIProductReconciler) Reconcile() (*capabilitiesv1beta2.Product, error) { desired, err := p.desired() if err != nil { return nil, err @@ -68,10 +69,10 @@ func (p *OpenAPIProductReconciler) Reconcile() (*capabilitiesv1beta1.Product, er p.Logger().V(1).Info(string(jsonData)) } - return nil, p.ReconcileResource(&capabilitiesv1beta1.Product{}, desired, p.productMutator) + return nil, p.ReconcileResource(&capabilitiesv1beta2.Product{}, desired, p.productMutator) } -func (p *OpenAPIProductReconciler) desired() (*capabilitiesv1beta1.Product, error) { +func (p *OpenAPIProductReconciler) desired() (*capabilitiesv1beta2.Product, error) { fieldErrors := field.ErrorList{} specFldPath := field.NewPath("spec") openapiRefFldPath := specFldPath.Child("openapiRef") @@ -100,16 +101,16 @@ func (p *OpenAPIProductReconciler) desired() (*capabilitiesv1beta1.Product, erro // product description description := fmt.Sprintf(p.openapiObj.Info.Description) - product := &capabilitiesv1beta1.Product{ + product := &capabilitiesv1beta2.Product{ TypeMeta: metav1.TypeMeta{ - Kind: capabilitiesv1beta1.ProductKind, - APIVersion: capabilitiesv1beta1.GroupVersion.String(), + Kind: capabilitiesv1beta2.ProductKind, + APIVersion: capabilitiesv1beta2.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: objName, Namespace: p.openapiCR.Namespace, }, - Spec: capabilitiesv1beta1.ProductSpec{ + Spec: capabilitiesv1beta2.ProductSpec{ Name: name, SystemName: systemName, Description: description, @@ -133,8 +134,8 @@ func (p *OpenAPIProductReconciler) desired() (*capabilitiesv1beta1.Product, erro // backend usages // current implementation assumes same system name for backend and product backendSystemName := p.desiredSystemName() - product.Spec.BackendUsages = map[string]capabilitiesv1beta1.BackendUsageSpec{ - backendSystemName: capabilitiesv1beta1.BackendUsageSpec{ + product.Spec.BackendUsages = map[string]capabilitiesv1beta2.BackendUsageSpec{ + backendSystemName: capabilitiesv1beta2.BackendUsageSpec{ Path: "/", }, } @@ -156,13 +157,13 @@ func (p *OpenAPIProductReconciler) desired() (*capabilitiesv1beta1.Product, erro } func (p *OpenAPIProductReconciler) productMutator(existingObj, desiredObj common.KubernetesObject) (bool, error) { - existing, ok := existingObj.(*capabilitiesv1beta1.Product) + existing, ok := existingObj.(*capabilitiesv1beta2.Product) if !ok { - return false, fmt.Errorf("%T is not a *capabilitiesv1beta1.Product", existingObj) + return false, fmt.Errorf("%T is not a *capabilitiesv1beta2.Product", existingObj) } - desired, ok := desiredObj.(*capabilitiesv1beta1.Product) + desired, ok := desiredObj.(*capabilitiesv1beta2.Product) if !ok { - return false, fmt.Errorf("%T is not a *capabilitiesv1beta1.Product", desiredObj) + return false, fmt.Errorf("%T is not a *capabilitiesv1beta2.Product", desiredObj) } // Metadata labels and annotations @@ -208,19 +209,19 @@ func (p *OpenAPIProductReconciler) desiredObjName() string { return fmt.Sprintf("%s-%s", helper.K8sNameFromOpenAPITitle(p.openapiObj), string(p.openapiCR.UID)) } -func (p *OpenAPIProductReconciler) desiredDeployment() *capabilitiesv1beta1.ProductDeploymentSpec { - deployment := &capabilitiesv1beta1.ProductDeploymentSpec{} +func (p *OpenAPIProductReconciler) desiredDeployment() *capabilitiesv1beta2.ProductDeploymentSpec { + deployment := &capabilitiesv1beta2.ProductDeploymentSpec{} if p.openapiCR.Spec.ProductionPublicBaseURL != nil || p.openapiCR.Spec.StagingPublicBaseURL != nil { // Self managed deployment - deployment.ApicastSelfManaged = &capabilitiesv1beta1.ApicastSelfManagedSpec{ + deployment.ApicastSelfManaged = &capabilitiesv1beta2.ApicastSelfManagedSpec{ StagingPublicBaseURL: p.openapiCR.Spec.StagingPublicBaseURL, ProductionPublicBaseURL: p.openapiCR.Spec.ProductionPublicBaseURL, Authentication: p.desiredAuthentication(), } } else { // Hosted deployment - deployment.ApicastHosted = &capabilitiesv1beta1.ApicastHostedSpec{ + deployment.ApicastHosted = &capabilitiesv1beta2.ApicastHostedSpec{ Authentication: p.desiredAuthentication(), } } @@ -228,7 +229,7 @@ func (p *OpenAPIProductReconciler) desiredDeployment() *capabilitiesv1beta1.Prod return deployment } -func (p *OpenAPIProductReconciler) desiredAuthentication() *capabilitiesv1beta1.AuthenticationSpec { +func (p *OpenAPIProductReconciler) desiredAuthentication() *capabilitiesv1beta2.AuthenticationSpec { globalSecRequirements := helper.OpenAPIGlobalSecurityRequirements(p.openapiObj) if len(globalSecRequirements) == 0 { // if no security requirements are found, default to UserKey auth @@ -238,7 +239,7 @@ func (p *OpenAPIProductReconciler) desiredAuthentication() *capabilitiesv1beta1. // Only the first one is used secRequirementExtended := globalSecRequirements[0] - var authenticationSpec *capabilitiesv1beta1.AuthenticationSpec + var authenticationSpec *capabilitiesv1beta2.AuthenticationSpec switch secRequirementExtended.Value.Type { // TODO types "oauth2", "openIdConnect" @@ -249,9 +250,9 @@ func (p *OpenAPIProductReconciler) desiredAuthentication() *capabilitiesv1beta1. return authenticationSpec } -func (p *OpenAPIProductReconciler) desiredUserKeyAuthentication(secReq *helper.ExtendedSecurityRequirement) *capabilitiesv1beta1.AuthenticationSpec { - authSpec := &capabilitiesv1beta1.AuthenticationSpec{ - UserKeyAuthentication: &capabilitiesv1beta1.UserKeyAuthenticationSpec{ +func (p *OpenAPIProductReconciler) desiredUserKeyAuthentication(secReq *helper.ExtendedSecurityRequirement) *capabilitiesv1beta2.AuthenticationSpec { + authSpec := &capabilitiesv1beta2.AuthenticationSpec{ + UserKeyAuthentication: &capabilitiesv1beta2.UserKeyAuthenticationSpec{ Security: p.desiredPrivateAPISecurity(), }, } @@ -277,12 +278,12 @@ func (p *OpenAPIProductReconciler) parseUserKeyCredentialsLoc(inField string) *s } } -func (p *OpenAPIProductReconciler) desiredMethods() map[string]capabilitiesv1beta1.MethodSpec { - methods := make(map[string]capabilitiesv1beta1.MethodSpec) +func (p *OpenAPIProductReconciler) desiredMethods() map[string]capabilitiesv1beta2.MethodSpec { + methods := make(map[string]capabilitiesv1beta2.MethodSpec) for path, pathItem := range p.openapiObj.Paths { for opVerb, operation := range pathItem.Operations() { methodSystemName := helper.MethodSystemNameFromOpenAPIOperation(path, opVerb, operation) - methods[methodSystemName] = capabilitiesv1beta1.MethodSpec{ + methods[methodSystemName] = capabilitiesv1beta2.MethodSpec{ Name: helper.MethodNameFromOpenAPIOperation(path, opVerb, operation), Description: operation.Description, } @@ -291,8 +292,8 @@ func (p *OpenAPIProductReconciler) desiredMethods() map[string]capabilitiesv1bet return methods } -func (p *OpenAPIProductReconciler) desiredMappingRules() ([]capabilitiesv1beta1.MappingRuleSpec, error) { - mappingRules := make([]capabilitiesv1beta1.MappingRuleSpec, 0) +func (p *OpenAPIProductReconciler) desiredMappingRules() ([]capabilitiesv1beta2.MappingRuleSpec, error) { + mappingRules := make([]capabilitiesv1beta2.MappingRuleSpec, 0) for path, pathItem := range p.openapiObj.Paths { desiredPattern, err := p.desiredMappingRulesPattern(path) if err != nil { @@ -300,7 +301,7 @@ func (p *OpenAPIProductReconciler) desiredMappingRules() ([]capabilitiesv1beta1. } for opVerb, operation := range pathItem.Operations() { - mappingRules = append(mappingRules, capabilitiesv1beta1.MappingRuleSpec{ + mappingRules = append(mappingRules, capabilitiesv1beta2.MappingRuleSpec{ HTTPMethod: strings.ToUpper(opVerb), Pattern: desiredPattern, MetricMethodRef: helper.MethodSystemNameFromOpenAPIOperation(path, opVerb, operation), @@ -352,12 +353,12 @@ func (p *OpenAPIProductReconciler) desiredPublicBasePath() (string, error) { return basePath, nil } -func (p *OpenAPIProductReconciler) desiredPrivateAPISecurity() *capabilitiesv1beta1.SecuritySpec { +func (p *OpenAPIProductReconciler) desiredPrivateAPISecurity() *capabilitiesv1beta2.SecuritySpec { if p.openapiCR.Spec.PrivateAPIHostHeader == nil && p.openapiCR.Spec.PrivateAPISecretToken == nil { return nil } - privateAPISec := &capabilitiesv1beta1.SecuritySpec{} + privateAPISec := &capabilitiesv1beta2.SecuritySpec{} if p.openapiCR.Spec.PrivateAPIHostHeader != nil { privateAPISec.HostHeader = p.openapiCR.Spec.PrivateAPIHostHeader diff --git a/controllers/capabilities/openapi_status_reconciler.go b/controllers/capabilities/openapi_status_reconciler.go index 98ce081f3..bb9f94769 100644 --- a/controllers/capabilities/openapi_status_reconciler.go +++ b/controllers/capabilities/openapi_status_reconciler.go @@ -4,6 +4,7 @@ import ( "fmt" capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" @@ -147,7 +148,7 @@ func (s *OpenAPIStatusReconciler) getManagedProduct() (*corev1.LocalObjectRefere listOps := []client.ListOption{ client.InNamespace(s.resource.Namespace), } - productList := &capabilitiesv1beta1.ProductList{} + productList := &capabilitiesv1beta2.ProductList{} err := s.Client().List(s.Context(), productList, listOps...) if err != nil { return nil, fmt.Errorf("Failed to list product: %w", err) diff --git a/controllers/capabilities/product_controller.go b/controllers/capabilities/product_controller.go index fd0aea871..f269e36fb 100644 --- a/controllers/capabilities/product_controller.go +++ b/controllers/capabilities/product_controller.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" threescaleapi "github.com/3scale/3scale-porta-go-client/client" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -58,7 +59,7 @@ func (r *ProductReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct reqLogger.Info("Reconcile Product", "Operator version", version.Version) // Fetch the Product instance - product := &capabilitiesv1beta1.Product{} + product := &capabilitiesv1beta2.Product{} err := r.Client().Get(r.Context(), req.NamespacedName, product) if err != nil { if errors.IsNotFound(err) { @@ -185,7 +186,7 @@ func (r *ProductReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, reconcileErr } -func (r *ProductReconciler) reconcile(productResource *capabilitiesv1beta1.Product) (*ProductStatusReconciler, error) { +func (r *ProductReconciler) reconcile(productResource *capabilitiesv1beta2.Product) (*ProductStatusReconciler, error) { logger := r.Logger().WithValues("product", productResource.Name) err := r.validateSpec(productResource) @@ -225,7 +226,7 @@ func (r *ProductReconciler) reconcile(productResource *capabilitiesv1beta1.Produ return statusReconciler, err } -func (r *ProductReconciler) validateSpec(resource *capabilitiesv1beta1.Product) error { +func (r *ProductReconciler) validateSpec(resource *capabilitiesv1beta2.Product) error { errors := field.ErrorList{} errors = append(errors, resource.Validate()...) @@ -239,7 +240,7 @@ func (r *ProductReconciler) validateSpec(resource *capabilitiesv1beta1.Product) } } -func (r *ProductReconciler) checkExternalRefs(resource *capabilitiesv1beta1.Product, providerAccount *controllerhelper.ProviderAccount) error { +func (r *ProductReconciler) checkExternalRefs(resource *capabilitiesv1beta2.Product, providerAccount *controllerhelper.ProviderAccount) error { logger := r.Logger().WithValues("product", resource.Name) errors := field.ErrorList{} @@ -269,7 +270,7 @@ func (r *ProductReconciler) checkExternalRefs(resource *capabilitiesv1beta1.Prod } } -func (r *ProductReconciler) checkBackendUsages(resource *capabilitiesv1beta1.Product, backendList []capabilitiesv1beta1.Backend) field.ErrorList { +func (r *ProductReconciler) checkBackendUsages(resource *capabilitiesv1beta2.Product, backendList []capabilitiesv1beta1.Backend) field.ErrorList { errors := field.ErrorList{} specFldPath := field.NewPath("spec") @@ -285,7 +286,7 @@ func (r *ProductReconciler) checkBackendUsages(resource *capabilitiesv1beta1.Pro return errors } -func checkAppLimitsExternalRefs(resource *capabilitiesv1beta1.Product, backendList []capabilitiesv1beta1.Backend) field.ErrorList { +func checkAppLimitsExternalRefs(resource *capabilitiesv1beta2.Product, backendList []capabilitiesv1beta1.Backend) field.ErrorList { // backendList param is expected to be valid product's backendUsageList errors := field.ErrorList{} @@ -321,7 +322,7 @@ func checkAppLimitsExternalRefs(resource *capabilitiesv1beta1.Product, backendLi return errors } -func checkAppPricingRulesExternalRefs(resource *capabilitiesv1beta1.Product, backendList []capabilitiesv1beta1.Backend) field.ErrorList { +func checkAppPricingRulesExternalRefs(resource *capabilitiesv1beta2.Product, backendList []capabilitiesv1beta1.Backend) field.ErrorList { // backendList param is expected to be valid product's backendUsageList errors := field.ErrorList{} @@ -366,7 +367,7 @@ func findBackendBySystemName(list []capabilitiesv1beta1.Backend, systemName stri return -1 } -func computeBackendUsageList(list []capabilitiesv1beta1.Backend, backendUsageMap map[string]capabilitiesv1beta1.BackendUsageSpec) []capabilitiesv1beta1.Backend { +func computeBackendUsageList(list []capabilitiesv1beta1.Backend, backendUsageMap map[string]capabilitiesv1beta2.BackendUsageSpec) []capabilitiesv1beta1.Backend { target := map[string]bool{} for systemName := range backendUsageMap { target[systemName] = true @@ -382,7 +383,7 @@ func computeBackendUsageList(list []capabilitiesv1beta1.Backend, backendUsageMap return result } -func (r *ProductReconciler) removeProductFrom3scale(product *capabilitiesv1beta1.Product) error { +func (r *ProductReconciler) removeProductFrom3scale(product *capabilitiesv1beta2.Product) error { logger := r.Logger().WithValues("product", client.ObjectKey{Name: product.Name, Namespace: product.Namespace}) // Attempt to remove product only if product.Status.ID is present @@ -415,6 +416,6 @@ func (r *ProductReconciler) removeProductFrom3scale(product *capabilitiesv1beta1 func (r *ProductReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&capabilitiesv1beta1.Product{}). + For(&capabilitiesv1beta2.Product{}). Complete(r) } diff --git a/controllers/capabilities/product_policies.go b/controllers/capabilities/product_policies.go index 3ccb1aaf6..a4ecea199 100644 --- a/controllers/capabilities/product_policies.go +++ b/controllers/capabilities/product_policies.go @@ -39,22 +39,27 @@ func (t *ProductThreescaleReconciler) convertResourcePolicies() *threescaleapi.P } for _, crdPolicy := range t.resource.Spec.Policies { - var configuration map[string]interface{} - // CRD validation ensures no error happens - // "configuration` type is object - //properties: - // configuration: - // description: Configuration defines the policy configuration - // type: object - // x-kubernetes-preserve-unknown-fields: true - _ = json.Unmarshal(crdPolicy.Configuration.Raw, &configuration) - - policies.Policies = append(policies.Policies, threescaleapi.PolicyConfig{ - Name: crdPolicy.Name, - Version: crdPolicy.Version, - Enabled: crdPolicy.Enabled, - Configuration: configuration, - }) + if crdPolicy.Configuration.Value.String() != "" { + var configuration map[string]interface{} + // CRD validation ensures no error happens + // "configuration` type is object + //properties: + // configuration: + // description: Configuration defines the policy configuration + // type: object + // x-kubernetes-preserve-unknown-fields: true + _ = json.Unmarshal(crdPolicy.Configuration.Value.Raw, &configuration) + + policies.Policies = append(policies.Policies, threescaleapi.PolicyConfig{ + Name: crdPolicy.Name, + Version: crdPolicy.Version, + Enabled: crdPolicy.Enabled, + Configuration: configuration, + }) + } + + // TODO: If fetching from secret + } return policies diff --git a/controllers/capabilities/product_status_reconciler.go b/controllers/capabilities/product_status_reconciler.go index 146bdc5bf..cfabb9ac6 100644 --- a/controllers/capabilities/product_status_reconciler.go +++ b/controllers/capabilities/product_status_reconciler.go @@ -3,7 +3,7 @@ package controllers import ( "fmt" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" @@ -17,14 +17,14 @@ import ( type ProductStatusReconciler struct { *reconcilers.BaseReconciler - resource *capabilitiesv1beta1.Product + resource *capabilitiesv1beta2.Product entity *controllerhelper.ProductEntity providerAccountHost string syncError error logger logr.Logger } -func NewProductStatusReconciler(b *reconcilers.BaseReconciler, resource *capabilitiesv1beta1.Product, entity *controllerhelper.ProductEntity, providerAccountHost string, syncError error) *ProductStatusReconciler { +func NewProductStatusReconciler(b *reconcilers.BaseReconciler, resource *capabilitiesv1beta2.Product, entity *controllerhelper.ProductEntity, providerAccountHost string, syncError error) *ProductStatusReconciler { return &ProductStatusReconciler{ BaseReconciler: b, resource: resource, @@ -71,8 +71,8 @@ func (s *ProductStatusReconciler) Reconcile() (reconcile.Result, error) { return reconcile.Result{}, nil } -func (s *ProductStatusReconciler) calculateStatus() *capabilitiesv1beta1.ProductStatus { - newStatus := &capabilitiesv1beta1.ProductStatus{} +func (s *ProductStatusReconciler) calculateStatus() *capabilitiesv1beta2.ProductStatus { + newStatus := &capabilitiesv1beta2.ProductStatus{} if s.entity != nil { tmpID := s.entity.ID() newStatus.ID = &tmpID @@ -93,7 +93,7 @@ func (s *ProductStatusReconciler) calculateStatus() *capabilitiesv1beta1.Product func (s *ProductStatusReconciler) syncCondition() common.Condition { condition := common.Condition{ - Type: capabilitiesv1beta1.ProductSyncedConditionType, + Type: capabilitiesv1beta2.ProductSyncedConditionType, Status: corev1.ConditionFalse, } @@ -106,7 +106,7 @@ func (s *ProductStatusReconciler) syncCondition() common.Condition { func (s *ProductStatusReconciler) orphanCondition() common.Condition { condition := common.Condition{ - Type: capabilitiesv1beta1.ProductOrphanConditionType, + Type: capabilitiesv1beta2.ProductOrphanConditionType, Status: corev1.ConditionFalse, } @@ -120,7 +120,7 @@ func (s *ProductStatusReconciler) orphanCondition() common.Condition { func (s *ProductStatusReconciler) invalidCondition() common.Condition { condition := common.Condition{ - Type: capabilitiesv1beta1.ProductInvalidConditionType, + Type: capabilitiesv1beta2.ProductInvalidConditionType, Status: corev1.ConditionFalse, } @@ -134,7 +134,7 @@ func (s *ProductStatusReconciler) invalidCondition() common.Condition { func (s *ProductStatusReconciler) failedCondition() common.Condition { condition := common.Condition{ - Type: capabilitiesv1beta1.ProductFailedConditionType, + Type: capabilitiesv1beta2.ProductFailedConditionType, Status: corev1.ConditionFalse, } diff --git a/controllers/capabilities/product_threescale_reconciler.go b/controllers/capabilities/product_threescale_reconciler.go index 169d1b460..9eaa483ce 100644 --- a/controllers/capabilities/product_threescale_reconciler.go +++ b/controllers/capabilities/product_threescale_reconciler.go @@ -3,7 +3,7 @@ package controllers import ( "fmt" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" @@ -14,14 +14,14 @@ import ( type ProductThreescaleReconciler struct { *reconcilers.BaseReconciler - resource *capabilitiesv1beta1.Product + resource *capabilitiesv1beta2.Product productEntity *controllerhelper.ProductEntity backendRemoteIndex *controllerhelper.BackendAPIRemoteIndex threescaleAPIClient *threescaleapi.ThreeScaleClient logger logr.Logger } -func NewProductThreescaleReconciler(b *reconcilers.BaseReconciler, resource *capabilitiesv1beta1.Product, threescaleAPIClient *threescaleapi.ThreeScaleClient, backendRemoteIndex *controllerhelper.BackendAPIRemoteIndex) *ProductThreescaleReconciler { +func NewProductThreescaleReconciler(b *reconcilers.BaseReconciler, resource *capabilitiesv1beta2.Product, threescaleAPIClient *threescaleapi.ThreeScaleClient, backendRemoteIndex *controllerhelper.BackendAPIRemoteIndex) *ProductThreescaleReconciler { return &ProductThreescaleReconciler{ BaseReconciler: b, resource: resource, diff --git a/controllers/capabilities/proxyconfigpromote_controller.go b/controllers/capabilities/proxyconfigpromote_controller.go index ea3513a8b..0aeebdee0 100644 --- a/controllers/capabilities/proxyconfigpromote_controller.go +++ b/controllers/capabilities/proxyconfigpromote_controller.go @@ -20,7 +20,10 @@ import ( "context" "encoding/json" "fmt" + "strconv" + capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" controllerhelper "github.com/3scale/3scale-operator/pkg/controller/helper" "github.com/3scale/3scale-operator/pkg/reconcilers" "github.com/3scale/3scale-operator/version" @@ -29,7 +32,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - "strconv" ) // ProxyConfigPromoteReconciler reconciles a ProxyConfigPromote object @@ -66,7 +68,7 @@ func (r *ProxyConfigPromoteReconciler) Reconcile(ctx context.Context, req ctrl.R reqLogger.V(1).Info(string(jsonData)) } // get product - product := &capabilitiesv1beta1.Product{} + product := &capabilitiesv1beta2.Product{} projectMeta := types.NamespacedName{ Name: proxyConfigPromote.Spec.ProductCRName, Namespace: req.Namespace, @@ -121,13 +123,13 @@ func (r *ProxyConfigPromoteReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } -func (r *ProxyConfigPromoteReconciler) proxyConfigPromoteReconciler(proxyConfigPromote *capabilitiesv1beta1.ProxyConfigPromote, reqLogger logr.Logger, threescaleAPIClient *threescaleapi.ThreeScaleClient, product *capabilitiesv1beta1.Product) (*ProxyConfigPromoteStatusReconciler, error) { +func (r *ProxyConfigPromoteReconciler) proxyConfigPromoteReconciler(proxyConfigPromote *capabilitiesv1beta1.ProxyConfigPromote, reqLogger logr.Logger, threescaleAPIClient *threescaleapi.ThreeScaleClient, product *capabilitiesv1beta2.Product) (*ProxyConfigPromoteStatusReconciler, error) { var latestStagingVersion int var latestProductionVersion int //get product - if product.Status.Conditions.IsTrueFor(capabilitiesv1beta1.ProductSyncedConditionType) { + if product.Status.Conditions.IsTrueFor(capabilitiesv1beta2.ProductSyncedConditionType) { productID := product.Status.ID productIDInt64 := *productID productIDStr := strconv.Itoa(int(productIDInt64)) diff --git a/controllers/capabilities/proxyconfigpromote_controller_test.go b/controllers/capabilities/proxyconfigpromote_controller_test.go index 71c5f57f6..b3f97f187 100644 --- a/controllers/capabilities/proxyconfigpromote_controller_test.go +++ b/controllers/capabilities/proxyconfigpromote_controller_test.go @@ -4,19 +4,21 @@ import ( "bytes" "encoding/json" "fmt" + "io/ioutil" + "net/http" + "reflect" + "testing" + "time" + capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" "github.com/3scale/3scale-operator/pkg/reconcilers" "github.com/3scale/3scale-porta-go-client/client" "github.com/go-logr/logr" - "io/ioutil" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "net/http" - "reflect" logf "sigs.k8s.io/controller-runtime/pkg/log" - "testing" - "time" ) func create(x int64) *int64 { @@ -83,22 +85,22 @@ func getProxyConfigPromoteCRProduction() (CR *capabilitiesv1beta1.ProxyConfigPro return CR } -func getProductList() (productList *capabilitiesv1beta1.ProductList) { - productList = &capabilitiesv1beta1.ProductList{ +func getProductList() (productList *capabilitiesv1beta2.ProductList) { + productList = &capabilitiesv1beta2.ProductList{ TypeMeta: metav1.TypeMeta{}, ListMeta: metav1.ListMeta{}, - Items: []capabilitiesv1beta1.Product{ + Items: []capabilitiesv1beta2.Product{ {TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "test", }, - Spec: capabilitiesv1beta1.ProductSpec{ + Spec: capabilitiesv1beta2.ProductSpec{ Name: "test", SystemName: "test", Description: "test", }, - Status: capabilitiesv1beta1.ProductStatus{ + Status: capabilitiesv1beta2.ProductStatus{ ID: create(3), ProviderAccountHost: "some string", ObservedGeneration: 1, @@ -110,25 +112,25 @@ func getProductList() (productList *capabilitiesv1beta1.ProductList) { return productList } -func getProductCR() (CR *capabilitiesv1beta1.Product) { +func getProductCR() (CR *capabilitiesv1beta2.Product) { - CR = &capabilitiesv1beta1.Product{ + CR = &capabilitiesv1beta2.Product{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "test", }, - Spec: capabilitiesv1beta1.ProductSpec{ + Spec: capabilitiesv1beta2.ProductSpec{ Name: "test", SystemName: "test", Description: "test", }, - Status: capabilitiesv1beta1.ProductStatus{ + Status: capabilitiesv1beta2.ProductStatus{ ID: create(3), ProviderAccountHost: "some string", ObservedGeneration: 1, Conditions: common.Conditions{common.Condition{ - Type: capabilitiesv1beta1.ProductSyncedConditionType, + Type: capabilitiesv1beta2.ProductSyncedConditionType, Status: v1.ConditionTrue, }}, }, @@ -304,7 +306,7 @@ func TestProxyConfigPromoteReconciler_proxyConfigPromoteReconciler(t *testing.T) proxyConfigPromote *capabilitiesv1beta1.ProxyConfigPromote reqLogger logr.Logger threescaleAPIClient *client.ThreeScaleClient - product *capabilitiesv1beta1.Product + product *capabilitiesv1beta2.Product } tests := []struct { name string diff --git a/main.go b/main.go index ee5fd0b35..c587a62c2 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,7 @@ import ( appsv1alpha1 "github.com/3scale/3scale-operator/apis/apps/v1alpha1" capabilitiesv1alpha1 "github.com/3scale/3scale-operator/apis/capabilities/v1alpha1" capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" appscontroller "github.com/3scale/3scale-operator/controllers/apps" capabilitiescontroller "github.com/3scale/3scale-operator/controllers/capabilities" "github.com/3scale/3scale-operator/pkg/3scale/amp/product" @@ -77,6 +78,7 @@ func init() { utilruntime.Must(grafanav1alpha1.AddToScheme(scheme)) utilruntime.Must(configv1.AddToScheme(scheme)) + utilruntime.Must(capabilitiesv1beta2.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -349,6 +351,16 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Application") os.Exit(1) } + + // Allow disabling running webhooks to run locally + // https://github.com/kubernetes-sigs/kubebuilder/issues/1501 + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&capabilitiesv1beta2.Product{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Product") + os.Exit(1) + } + } + // +kubebuilder:scaffold:builder setupLog.Info("starting manager") diff --git a/pkg/controller/helper/product_list.go b/pkg/controller/helper/product_list.go index 2243d3285..96c0f73d3 100644 --- a/pkg/controller/helper/product_list.go +++ b/pkg/controller/helper/product_list.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -14,8 +14,8 @@ import ( // ProductList returns a list of product custom resources where all elements: // - Sync state (ensure remote product exist and in sync) // - Same 3scale provider Account -func ProductList(ns string, cl client.Client, providerAccountURLStr string, logger logr.Logger) ([]capabilitiesv1beta1.Product, error) { - productList := &capabilitiesv1beta1.ProductList{} +func ProductList(ns string, cl client.Client, providerAccountURLStr string, logger logr.Logger) ([]capabilitiesv1beta2.Product, error) { + productList := &capabilitiesv1beta2.ProductList{} opts := []controllerclient.ListOption{ controllerclient.InNamespace(ns), } @@ -26,7 +26,7 @@ func ProductList(ns string, cl client.Client, providerAccountURLStr string, logg } logger.V(1).Info("Product resources", "total", len(productList.Items)) - validProducts := make([]capabilitiesv1beta1.Product, 0) + validProducts := make([]capabilitiesv1beta2.Product, 0) for idx := range productList.Items { // Filter by synchronized if !productList.Items[idx].IsSynced() { @@ -49,7 +49,7 @@ func ProductList(ns string, cl client.Client, providerAccountURLStr string, logg return validProducts, nil } -func FindProductBySystemName(list []capabilitiesv1beta1.Product, systemName string) int { +func FindProductBySystemName(list []capabilitiesv1beta2.Product, systemName string) int { for idx := range list { if list[idx].Spec.SystemName == systemName { return idx diff --git a/pkg/controller/helper/product_list_test.go b/pkg/controller/helper/product_list_test.go index 26eedacab..c3830be66 100644 --- a/pkg/controller/helper/product_list_test.go +++ b/pkg/controller/helper/product_list_test.go @@ -3,7 +3,7 @@ package helper import ( "testing" - capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1" + capabilitiesv1beta2 "github.com/3scale/3scale-operator/apis/capabilities/v1beta2" "github.com/3scale/3scale-operator/pkg/common" "github.com/go-logr/logr" @@ -32,25 +32,25 @@ func TestProductList(t *testing.T) { anotherProviderSecret := GetTestSecret(ns, anotherProviderSecretName, data) s := scheme.Scheme - err := capabilitiesv1beta1.AddToScheme(s) + err := capabilitiesv1beta2.AddToScheme(s) if err != nil { t.Fatalf("Unable to add Apps scheme: (%v)", err) } cases := []struct { testName string - product *capabilitiesv1beta1.Product + product *capabilitiesv1beta2.Product expected bool }{ { "sync'ed product and same providerAccount", - &capabilitiesv1beta1.Product{ + &capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{Name: "somename", Namespace: ns}, - Spec: capabilitiesv1beta1.ProductSpec{}, - Status: capabilitiesv1beta1.ProductStatus{ + Spec: capabilitiesv1beta2.ProductSpec{}, + Status: capabilitiesv1beta2.ProductStatus{ Conditions: common.Conditions{ common.Condition{ - Type: capabilitiesv1beta1.ProductSyncedConditionType, + Type: capabilitiesv1beta2.ProductSyncedConditionType, Status: corev1.ConditionTrue, }, }, @@ -60,13 +60,13 @@ func TestProductList(t *testing.T) { }, { "Not sync'ed product", - &capabilitiesv1beta1.Product{ + &capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{Name: "somename", Namespace: ns}, - Spec: capabilitiesv1beta1.ProductSpec{}, - Status: capabilitiesv1beta1.ProductStatus{ + Spec: capabilitiesv1beta2.ProductSpec{}, + Status: capabilitiesv1beta2.ProductStatus{ Conditions: common.Conditions{ common.Condition{ - Type: capabilitiesv1beta1.ProductSyncedConditionType, + Type: capabilitiesv1beta2.ProductSyncedConditionType, Status: corev1.ConditionFalse, }, }, @@ -76,17 +76,17 @@ func TestProductList(t *testing.T) { }, { "provider not matching product", - &capabilitiesv1beta1.Product{ + &capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{Name: "somename", Namespace: ns}, - Spec: capabilitiesv1beta1.ProductSpec{ + Spec: capabilitiesv1beta2.ProductSpec{ ProviderAccountRef: &corev1.LocalObjectReference{ Name: anotherProviderSecretName, }, }, - Status: capabilitiesv1beta1.ProductStatus{ + Status: capabilitiesv1beta2.ProductStatus{ Conditions: common.Conditions{ common.Condition{ - Type: capabilitiesv1beta1.ProductSyncedConditionType, + Type: capabilitiesv1beta2.ProductSyncedConditionType, Status: corev1.ConditionTrue, }, }, @@ -113,18 +113,18 @@ func TestProductList(t *testing.T) { func TestFindProductBySystemName(t *testing.T) { ns := "somenamespace" - productList := []capabilitiesv1beta1.Product{ - capabilitiesv1beta1.Product{ + productList := []capabilitiesv1beta2.Product{ + capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{Name: "A", Namespace: ns}, - Spec: capabilitiesv1beta1.ProductSpec{SystemName: "A"}, + Spec: capabilitiesv1beta2.ProductSpec{SystemName: "A"}, }, - capabilitiesv1beta1.Product{ + capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{Name: "B", Namespace: ns}, - Spec: capabilitiesv1beta1.ProductSpec{SystemName: "B"}, + Spec: capabilitiesv1beta2.ProductSpec{SystemName: "B"}, }, - capabilitiesv1beta1.Product{ + capabilitiesv1beta2.Product{ ObjectMeta: metav1.ObjectMeta{Name: "C", Namespace: ns}, - Spec: capabilitiesv1beta1.ProductSpec{SystemName: "C"}, + Spec: capabilitiesv1beta2.ProductSpec{SystemName: "C"}, }, }