diff --git a/api/v2/apisixupstream_types.go b/api/v2/apisixupstream_types.go index 552164218..23e47425d 100644 --- a/api/v2/apisixupstream_types.go +++ b/api/v2/apisixupstream_types.go @@ -29,6 +29,7 @@ type ApisixUpstreamSpec struct { // ExternalNodes contains external nodes the Upstream should use // If this field is set, the upstream will use these nodes directly without any further resolves // +kubebuilder:validation:Optional + // +kubebuilder:validation:MinItems=1 ExternalNodes []ApisixUpstreamExternalNode `json:"externalNodes,omitempty" yaml:"externalNodes,omitempty"` ApisixUpstreamConfig `json:",inline" yaml:",inline"` @@ -81,6 +82,7 @@ type ApisixUpstreamConfig struct { // The scheme used to talk with the upstream. // Now value can be http, grpc. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=http;https;grpc;grpcs; Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` // How many times that the proxy (Apache APISIX) should do when @@ -92,6 +94,7 @@ type ApisixUpstreamConfig struct { // +kubebuilder:validation:Optional Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + // Deprecated: this is no longer support on standalone mode. // The health check configurations for the upstream. // +kubebuilder:validation:Optional HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"` @@ -108,6 +111,7 @@ type ApisixUpstreamConfig struct { // Configures the host when the request is forwarded to the upstream. // Can be one of pass, node or rewrite. // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=pass;node;rewrite; PassHost string `json:"passHost,omitempty" yaml:"passHost,omitempty"` // Specifies the host of the Upstream request. This is only valid if @@ -115,6 +119,7 @@ type ApisixUpstreamConfig struct { // +kubebuilder:validation:Optional UpstreamHost string `json:"upstreamHost,omitempty" yaml:"upstreamHost,omitempty"` + // Deprecated: this is no longer support on standalone mode. // Discovery is used to configure service discovery for upstream. // +kubebuilder:validation:Optional Discovery *Discovery `json:"discovery,omitempty" yaml:"discovery,omitempty"` @@ -145,7 +150,9 @@ type LoadBalancer struct { // HealthCheck describes the upstream health check parameters. type HealthCheck struct { - Active *ActiveHealthCheck `json:"active" yaml:"active"` + // +kubebuilder:validation:Required + Active *ActiveHealthCheck `json:"active" yaml:"active"` + // +kubebuilder:validation:Optional Passive *PassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"` } @@ -159,17 +166,23 @@ type ApisixUpstreamSubset struct { // Discovery defines Service discovery related configuration. type Discovery struct { - ServiceName string `json:"serviceName" yaml:"serviceName"` - Type string `json:"type" yaml:"type"` - Args map[string]string `json:"args,omitempty" yaml:"args,omitempty"` + ServiceName string `json:"serviceName" yaml:"serviceName"` + Type string `json:"type" yaml:"type"` + // +kubebuilder:validation:Optional + Args map[string]string `json:"args,omitempty" yaml:"args,omitempty"` } // ActiveHealthCheck defines the active kind of upstream health check. type ActiveHealthCheck struct { - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` - Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"` - Host string `json:"host,omitempty" yaml:"host,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=http;https;tcp; + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` + // +kubebuilder:validation:Minimum=0 + Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=65535 Port int32 `json:"port,omitempty" yaml:"port,omitempty"` HTTPPath string `json:"httpPath,omitempty" yaml:"httpPath,omitempty"` StrictTLS *bool `json:"strictTLS,omitempty" yaml:"strictTLS,omitempty"` @@ -205,17 +218,27 @@ type ActiveHealthCheckUnhealthy struct { // PassiveHealthCheckHealthy defines the conditions to judge whether // an upstream node is healthy with the passive manner. type PassiveHealthCheckHealthy struct { + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MinItems=1 HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"` - Successes int `json:"successes,omitempty" yaml:"successes,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=254 + Successes int `json:"successes,omitempty" yaml:"successes,omitempty"` } // PassiveHealthCheckUnhealthy defines the conditions to judge whether // an upstream node is unhealthy with the passive manager. type PassiveHealthCheckUnhealthy struct { - HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"` - HTTPFailures int `json:"httpFailures,omitempty" yaml:"http_failures,omitempty"` - TCPFailures int `json:"tcpFailures,omitempty" yaml:"tcpFailures,omitempty"` - Timeouts int `json:"timeout,omitempty" yaml:"timeout,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MinItems=1 + HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=254 + HTTPFailures int `json:"httpFailures,omitempty" yaml:"http_failures,omitempty"` + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=254 + TCPFailures int `json:"tcpFailures,omitempty" yaml:"tcpFailures,omitempty"` + Timeouts int `json:"timeout,omitempty" yaml:"timeout,omitempty"` } func init() { diff --git a/api/v2/shared_types.go b/api/v2/shared_types.go index cda28a2f5..e654606fb 100644 --- a/api/v2/shared_types.go +++ b/api/v2/shared_types.go @@ -136,6 +136,22 @@ const ( ExternalTypeService ApisixUpstreamExternalType = "Service" ) +const ( + // HealthCheckHTTP represents the HTTP kind health check. + HealthCheckHTTP = "http" + // HealthCheckHTTPS represents the HTTPS kind health check. + HealthCheckHTTPS = "https" + // HealthCheckTCP represents the TCP kind health check. + HealthCheckTCP = "tcp" + + // HealthCheckMaxConsecutiveNumber is the max number for + // the consecutive success/failure in upstream health check. + HealthCheckMaxConsecutiveNumber = 254 + // ActiveHealthCheckMinInterval is the minimum interval for + // the active health check. + ActiveHealthCheckMinInterval = time.Second +) + var schemeToPortMaps = map[string]int{ SchemeHTTP: 80, SchemeHTTPS: 443, diff --git a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml index 542a0c5f3..9da6294ed 100644 --- a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml +++ b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml @@ -40,8 +40,9 @@ spec: description: ApisixUpstreamSpec describes the specification of ApisixUpstream. properties: discovery: - description: Discovery is used to configure service discovery for - upstream. + description: |- + Deprecated: this is no longer support on standalone mode. + Discovery is used to configure service discovery for upstream. properties: args: additionalProperties: @@ -74,15 +75,19 @@ spec: weight: type: integer type: object + minItems: 1 type: array healthCheck: - description: The health check configurations for the upstream. + description: |- + Deprecated: this is no longer support on standalone mode. + The health check configurations for the upstream. properties: active: description: ActiveHealthCheck defines the active kind of upstream health check. properties: concurrency: + minimum: 0 type: integer healthy: description: |- @@ -92,10 +97,13 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array interval: type: string successes: + maximum: 254 + minimum: 0 type: integer type: object host: @@ -104,6 +112,8 @@ spec: type: string port: format: int32 + maximum: 65535 + minimum: 0 type: integer requestHeaders: items: @@ -119,6 +129,10 @@ spec: format: int64 type: integer type: + enum: + - http + - https + - tcp type: string unhealthy: description: |- @@ -128,12 +142,17 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array httpFailures: + maximum: 254 + minimum: 0 type: integer interval: type: string tcpFailures: + maximum: 254 + minimum: 0 type: integer timeout: type: integer @@ -152,8 +171,11 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array successes: + maximum: 254 + minimum: 0 type: integer type: object type: @@ -166,10 +188,15 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array httpFailures: + maximum: 254 + minimum: 0 type: integer tcpFailures: + maximum: 254 + minimum: 0 type: integer timeout: type: integer @@ -207,6 +234,10 @@ spec: description: |- Configures the host when the request is forwarded to the upstream. Can be one of pass, node or rewrite. + enum: + - pass + - node + - rewrite type: string portLevelSettings: items: @@ -216,8 +247,9 @@ spec: them if they are set on the port level. properties: discovery: - description: Discovery is used to configure service discovery - for upstream. + description: |- + Deprecated: this is no longer support on standalone mode. + Discovery is used to configure service discovery for upstream. properties: args: additionalProperties: @@ -232,13 +264,16 @@ spec: - type type: object healthCheck: - description: The health check configurations for the upstream. + description: |- + Deprecated: this is no longer support on standalone mode. + The health check configurations for the upstream. properties: active: description: ActiveHealthCheck defines the active kind of upstream health check. properties: concurrency: + minimum: 0 type: integer healthy: description: |- @@ -248,10 +283,13 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array interval: type: string successes: + maximum: 254 + minimum: 0 type: integer type: object host: @@ -260,6 +298,8 @@ spec: type: string port: format: int32 + maximum: 65535 + minimum: 0 type: integer requestHeaders: items: @@ -275,6 +315,10 @@ spec: format: int64 type: integer type: + enum: + - http + - https + - tcp type: string unhealthy: description: |- @@ -284,12 +328,17 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array httpFailures: + maximum: 254 + minimum: 0 type: integer interval: type: string tcpFailures: + maximum: 254 + minimum: 0 type: integer timeout: type: integer @@ -308,8 +357,11 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array successes: + maximum: 254 + minimum: 0 type: integer type: object type: @@ -322,10 +374,15 @@ spec: httpCodes: items: type: integer + minItems: 1 type: array httpFailures: + maximum: 254 + minimum: 0 type: integer tcpFailures: + maximum: 254 + minimum: 0 type: integer timeout: type: integer @@ -356,6 +413,10 @@ spec: description: |- Configures the host when the request is forwarded to the upstream. Can be one of pass, node or rewrite. + enum: + - pass + - node + - rewrite type: string port: description: Port is a Kubernetes Service port, it should be @@ -372,6 +433,11 @@ spec: description: |- The scheme used to talk with the upstream. Now value can be http, grpc. + enum: + - http + - https + - grpc + - grpcs type: string subsets: description: |- @@ -438,6 +504,11 @@ spec: description: |- The scheme used to talk with the upstream. Now value can be http, grpc. + enum: + - http + - https + - grpc + - grpcs type: string subsets: description: |- diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 552fc1c41..516b74bec 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -15,6 +15,7 @@ rules: - "" resources: - namespaces + - pods - secrets - services verbs: diff --git a/docs/crd/api.md b/docs/crd/api.md index a337c2db5..2c1481dd7 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -1334,12 +1334,12 @@ load balancer, health check, etc. | `scheme` _string_ | The scheme used to talk with the upstream. Now value can be http, grpc. | | `retries` _integer_ | How many times that the proxy (Apache APISIX) should do when errors occur (error, timeout or bad http status codes like 500, 502). | | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout settings for the read, send and connect to the upstream. | -| `healthCheck` _[HealthCheck](#healthcheck)_ | The health check configurations for the upstream. | +| `healthCheck` _[HealthCheck](#healthcheck)_ | Deprecated: this is no longer support on standalone mode. The health check configurations for the upstream. | | `tlsSecret` _[ApisixSecret](#apisixsecret)_ | Set the client certificate when connecting to TLS upstream. | | `subsets` _[ApisixUpstreamSubset](#apisixupstreamsubset) array_ | Subsets groups the service endpoints by their labels. Usually used to differentiate service versions. | | `passHost` _string_ | Configures the host when the request is forwarded to the upstream. Can be one of pass, node or rewrite. | | `upstreamHost` _string_ | Specifies the host of the Upstream request. This is only valid if the pass_host is set to rewrite | -| `discovery` _[Discovery](#discovery)_ | Discovery is used to configure service discovery for upstream. | +| `discovery` _[Discovery](#discovery)_ | Deprecated: this is no longer support on standalone mode. Discovery is used to configure service discovery for upstream. | _Appears in:_ @@ -1391,12 +1391,12 @@ ApisixUpstreamSpec describes the specification of ApisixUpstream. | `scheme` _string_ | The scheme used to talk with the upstream. Now value can be http, grpc. | | `retries` _integer_ | How many times that the proxy (Apache APISIX) should do when errors occur (error, timeout or bad http status codes like 500, 502). | | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout settings for the read, send and connect to the upstream. | -| `healthCheck` _[HealthCheck](#healthcheck)_ | The health check configurations for the upstream. | +| `healthCheck` _[HealthCheck](#healthcheck)_ | Deprecated: this is no longer support on standalone mode. The health check configurations for the upstream. | | `tlsSecret` _[ApisixSecret](#apisixsecret)_ | Set the client certificate when connecting to TLS upstream. | | `subsets` _[ApisixUpstreamSubset](#apisixupstreamsubset) array_ | Subsets groups the service endpoints by their labels. Usually used to differentiate service versions. | | `passHost` _string_ | Configures the host when the request is forwarded to the upstream. Can be one of pass, node or rewrite. | | `upstreamHost` _string_ | Specifies the host of the Upstream request. This is only valid if the pass_host is set to rewrite | -| `discovery` _[Discovery](#discovery)_ | Discovery is used to configure service discovery for upstream. | +| `discovery` _[Discovery](#discovery)_ | Deprecated: this is no longer support on standalone mode. Discovery is used to configure service discovery for upstream. | | `portLevelSettings` _[PortLevelSettings](#portlevelsettings) array_ | | @@ -1560,12 +1560,12 @@ them if they are set on the port level. | `scheme` _string_ | The scheme used to talk with the upstream. Now value can be http, grpc. | | `retries` _integer_ | How many times that the proxy (Apache APISIX) should do when errors occur (error, timeout or bad http status codes like 500, 502). | | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout settings for the read, send and connect to the upstream. | -| `healthCheck` _[HealthCheck](#healthcheck)_ | The health check configurations for the upstream. | +| `healthCheck` _[HealthCheck](#healthcheck)_ | Deprecated: this is no longer support on standalone mode. The health check configurations for the upstream. | | `tlsSecret` _[ApisixSecret](#apisixsecret)_ | Set the client certificate when connecting to TLS upstream. | | `subsets` _[ApisixUpstreamSubset](#apisixupstreamsubset) array_ | Subsets groups the service endpoints by their labels. Usually used to differentiate service versions. | | `passHost` _string_ | Configures the host when the request is forwarded to the upstream. Can be one of pass, node or rewrite. | | `upstreamHost` _string_ | Specifies the host of the Upstream request. This is only valid if the pass_host is set to rewrite | -| `discovery` _[Discovery](#discovery)_ | Discovery is used to configure service discovery for upstream. | +| `discovery` _[Discovery](#discovery)_ | Deprecated: this is no longer support on standalone mode. Discovery is used to configure service discovery for upstream. | | `port` _integer_ | Port is a Kubernetes Service port, it should be already defined. | diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 4b52e530d..36c6d2e4d 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -343,7 +343,15 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid Message: fmt.Sprintf("failed to list endpoint slices: %v", err), } } - tc.EndpointSlices[serviceNN] = endpoints.Items + + // backend.subset specifies a subset of upstream nodes. + // It specifies that the target pod's label should be a superset of the subset labels of the ApisixUpstream of the serviceName + subsetLabels, err := r.getSubsetLabels(ctx, in, backend) + if err != nil { + return err + } + + tc.EndpointSlices[serviceNN] = r.filterEndpointSlicesBySubsetLabels(ctx, endpoints.Items, subsetLabels) } return nil @@ -684,3 +692,72 @@ func (r *ApisixRouteReconciler) listApisixRoutesForPluginConfig(ctx context.Cont } return pkgutils.DedupComparable(requests) } + +func (r *ApisixRouteReconciler) getSubsetLabels(ctx context.Context, ar *apiv2.ApisixRoute, backend apiv2.ApisixRouteHTTPBackend) (map[string]string, error) { + empty := make(map[string]string) + if backend.Subset == "" { + return empty, nil + } + + // Try to Get the ApisixUpstream with the same name as backend.ServiceName + var ( + auNN = types.NamespacedName{ + Namespace: ar.GetNamespace(), + Name: backend.ServiceName, + } + au apiv2.ApisixUpstream + ) + if err := r.Get(ctx, auNN, &au); err != nil { + if client.IgnoreNotFound(err) == nil { + return empty, nil + } + return nil, err + } + + // try to get the subset labels from the ApisixUpstream subsets + for _, subset := range au.Spec.Subsets { + if backend.Subset == subset.Name { + return subset.Labels, nil + } + } + + return empty, nil +} + +func (r *ApisixRouteReconciler) filterEndpointSlicesBySubsetLabels(ctx context.Context, in []discoveryv1.EndpointSlice, labels map[string]string) []discoveryv1.EndpointSlice { + if len(labels) == 0 { + return in + } + + for i := range in { + in[i] = r.filterEndpointSliceByTargetPod(ctx, in[i], labels) + } + + return utils.Filter(in, func(v discoveryv1.EndpointSlice) bool { + return len(v.Endpoints) > 0 + }) +} + +// filterEndpointSliceByTargetPod filters item.Endpoints which is not a subset of labels +func (r *ApisixRouteReconciler) filterEndpointSliceByTargetPod(ctx context.Context, item discoveryv1.EndpointSlice, labels map[string]string) discoveryv1.EndpointSlice { + item.Endpoints = utils.Filter(item.Endpoints, func(v discoveryv1.Endpoint) bool { + if v.TargetRef == nil || v.TargetRef.Kind != KindPod { + return true + } + + var ( + pod corev1.Pod + podNN = types.NamespacedName{ + Namespace: v.TargetRef.Namespace, + Name: v.TargetRef.Name, + } + ) + if err := r.Get(ctx, podNN, &pod); err != nil { + return false + } + + return utils.IsSubsetOf(labels, pod.GetLabels()) + }) + + return item +} diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index 1f0354aa0..fdc6af555 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -528,6 +528,11 @@ func ApisixRouteSecretIndexFunc(cli client.Client) func(client.Object) []string func ApisixRouteApisixUpstreamIndexFunc(obj client.Object) (keys []string) { ar := obj.(*apiv2.ApisixRoute) for _, rule := range ar.Spec.HTTP { + for _, backend := range rule.Backends { + if backend.Subset != "" && backend.ServiceName != "" { + keys = append(keys, GenIndexKey(ar.GetNamespace(), backend.ServiceName)) + } + } for _, upstream := range rule.Upstreams { if upstream.Name != "" { keys = append(keys, GenIndexKey(ar.GetNamespace(), upstream.Name)) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 6be766041..93b573dd0 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -58,6 +58,7 @@ const ( KindApisixRoute = "ApisixRoute" KindApisixGlobalRule = "ApisixGlobalRule" KindApisixPluginConfig = "ApisixPluginConfig" + KindPod = "Pod" KindApisixTls = "ApisixTls" ) diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index 5fb93bbcd..2ca4ddbe8 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -29,6 +29,7 @@ import ( // +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="discovery.k8s.io",resources=endpointslices,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch diff --git a/internal/provider/adc/translator/apisixupstream.go b/internal/provider/adc/translator/apisixupstream.go index 34b6f24f6..936a05284 100644 --- a/internal/provider/adc/translator/apisixupstream.go +++ b/internal/provider/adc/translator/apisixupstream.go @@ -27,25 +27,20 @@ import ( ) func (t *Translator) translateApisixUpstream(tctx *provider.TranslateContext, au *apiv2.ApisixUpstream) (ups *adc.Upstream, err error) { - if len(au.Spec.ExternalNodes) == 0 && au.Spec.Discovery == nil { - return nil, errors.Errorf("%s has empty externalNodes or discovery configuration", utils.NamespacedName(au)) - } - ups = adc.NewDefaultUpstream() for _, f := range []func(*apiv2.ApisixUpstream, *adc.Upstream) error{ + patchApisixUpstreamBasics, translateApisixUpstreamScheme, translateApisixUpstreamLoadBalancer, - translateApisixUpstreamHealthCheck, translateApisixUpstreamRetriesAndTimeout, - translateApisixUpstreamClientTLS, translateApisixUpstreamPassHost, - translateApisixUpstreamDiscovery, } { if err = f(au, ups); err != nil { return } } for _, f := range []func(*provider.TranslateContext, *apiv2.ApisixUpstream, *adc.Upstream) error{ + translateApisixUpstreamClientTLS, translateApisixUpstreamExternalNodes, } { if err = f(tctx, au, ups); err != nil { @@ -56,16 +51,19 @@ func (t *Translator) translateApisixUpstream(tctx *provider.TranslateContext, au return } -func translateApisixUpstreamScheme(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { - switch au.Spec.Scheme { - case apiv2.SchemeHTTP, apiv2.SchemeHTTPS, apiv2.SchemeGRPC, apiv2.SchemeGRPCS: - ups.Scheme = au.Spec.Scheme - default: - ups.Scheme = apiv2.SchemeHTTP +func patchApisixUpstreamBasics(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { + ups.Name = composeExternalUpstreamName(au) + for k, v := range au.Labels { + ups.Labels[k] = v } return nil } +func translateApisixUpstreamScheme(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { + ups.Scheme = cmp.Or(au.Spec.Scheme, apiv2.SchemeHTTP) + return nil +} + func translateApisixUpstreamLoadBalancer(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { lb := au.Spec.LoadBalancer if lb == nil || lb.Type == "" { @@ -98,11 +96,6 @@ func translateApisixUpstreamLoadBalancer(au *apiv2.ApisixUpstream, ups *adc.Upst return nil } -func translateApisixUpstreamHealthCheck(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { - // todo: handle `.Checks` in next PR - return nil -} - func translateApisixUpstreamRetriesAndTimeout(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { retries := au.Spec.Retries timeout := au.Spec.Timeout @@ -140,27 +133,44 @@ func translateApisixUpstreamRetriesAndTimeout(au *apiv2.ApisixUpstream, ups *adc return nil } -func translateApisixUpstreamClientTLS(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { - // todo: handle `.TLS` in next PR +func translateApisixUpstreamClientTLS(tctx *provider.TranslateContext, au *apiv2.ApisixUpstream, ups *adc.Upstream) error { + if au.Spec.TLSSecret == nil { + return nil + } + + var ( + secretNN = types.NamespacedName{ + Namespace: au.Spec.TLSSecret.Namespace, + Name: au.Spec.TLSSecret.Name, + } + ) + secret, ok := tctx.Secrets[secretNN] + if !ok { + return errors.Errorf("sercret %s not found", secretNN) + } + + cert, key, err := extractKeyPair(secret, true) + if err != nil { + return err + } + + ups.TLS = &adc.ClientTLS{ + Cert: string(cert), + Key: string(key), + } + return nil } func translateApisixUpstreamPassHost(au *apiv2.ApisixUpstream, ups *adc.Upstream) error { - switch passHost := au.Spec.PassHost; passHost { - case apiv2.PassHostPass, apiv2.PassHostNode, apiv2.PassHostRewrite: - ups.PassHost = passHost - default: - ups.PassHost = "" - } - + ups.PassHost = au.Spec.PassHost ups.UpstreamHost = au.Spec.UpstreamHost return nil } -func translateApisixUpstreamDiscovery(upstream *apiv2.ApisixUpstream, upstream2 *adc.Upstream) error { - // todo: handle `.Discovery*` in next PR - return nil +func composeExternalUpstreamName(au *apiv2.ApisixUpstream) string { + return au.GetGenerateName() + "_" + au.GetName() } func translateApisixUpstreamExternalNodes(tctx *provider.TranslateContext, au *apiv2.ApisixUpstream, ups *adc.Upstream) error { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index db52a3826..288811b45 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -63,8 +63,8 @@ func NewDefaultTranslateContext(ctx context.Context) *TranslateContext { PluginConfigs: make(map[k8stypes.NamespacedName]*v1alpha1.PluginConfig), ApisixPluginConfigs: make(map[k8stypes.NamespacedName]*apiv2.ApisixPluginConfig), Services: make(map[k8stypes.NamespacedName]*corev1.Service), - Upstreams: make(map[k8stypes.NamespacedName]*apiv2.ApisixUpstream), BackendTrafficPolicies: make(map[k8stypes.NamespacedName]*v1alpha1.BackendTrafficPolicy), + Upstreams: make(map[k8stypes.NamespacedName]*apiv2.ApisixUpstream), GatewayProxies: make(map[types.NamespacedNameKind]v1alpha1.GatewayProxy), ResourceParentRefs: make(map[types.NamespacedNameKind][]types.NamespacedNameKind), } diff --git a/internal/utils/k8s.go b/internal/utils/k8s.go index 7cc3767d5..d2e5abe48 100644 --- a/internal/utils/k8s.go +++ b/internal/utils/k8s.go @@ -60,3 +60,29 @@ var hostDefRegex = regexp.MustCompile(hostDef) func MatchHostDef(host string) bool { return hostDefRegex.MatchString(host) } + +func AppendFunc[T any](s []T, keep func(v T) bool, values ...T) []T { + for _, v := range values { + if keep(v) { + s = append(s, v) + } + } + return s +} + +func Filter[T any](s []T, keep func(v T) bool) []T { + return AppendFunc(make([]T, 0), keep, s...) +} + +func IsSubsetOf(a, b map[string]string) bool { + if len(a) == 0 { + // Empty labels matches everything. + return true + } + for k, v := range a { + if vv, ok := b[k]; !ok || vv != v { + return false + } + } + return true +} diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go index 67e1e5ffb..3e24082c7 100644 --- a/test/e2e/apisix/route.go +++ b/test/e2e/apisix/route.go @@ -291,10 +291,67 @@ spec: s.NewAPISIXClient().GET("/get").Expect().Header("X-Upstream-IP").IsEqual(clusterIP) }) - PIt("Test ApisixRoute subset", func() { - // route.Spec.HTTP[].Backends[].Subset depends on ApisixUpstream. - // ApisixUpstream is not implemented yet. - // So the case is pending for now + It("Test ApisixRoute subset", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + subset: test-subset +` + const apisixUpstreamSpec0 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test +spec: + ingressClassName: apisix + subsets: + - name: test-subset + labels: + unknown-key: unknown-value +` + const apisixUpstreamSpec1 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test +spec: + ingressClassName: apisix + subsets: + - name: test-subset + labels: + app: httpbin-deployment-e2e-test +` + request := func() int { + return s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Raw().StatusCode + } + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + // no pod matches the subset label "unknown-key: unknown-value" so there will be no node in the upstream, + // to request the route will get http.StatusServiceUnavailable + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec0) + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusServiceUnavailable)) + + // the pod matches the subset label "app: httpbin-deployment-e2e-test", + // to request the route will be OK + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec1) + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) }) }) @@ -351,8 +408,7 @@ spec: Expect(err).ShouldNot(HaveOccurred(), "apply service") applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec0) - var apisxiRoute apiv2.ApisixRoute - applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisxiRoute, apisixRouteSpec) + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec) By("verify that the ApisixUpstream reference a Service which is not ExternalName should not request OK") request := func(path string) int { diff --git a/test/e2e/framework/manifests/ingress.yaml b/test/e2e/framework/manifests/ingress.yaml index 1b086ff54..72dc0cf6a 100644 --- a/test/e2e/framework/manifests/ingress.yaml +++ b/test/e2e/framework/manifests/ingress.yaml @@ -82,6 +82,7 @@ rules: - "" resources: - services + - pods verbs: - get - list