feat: support apisix ingress as trafficrouting type
Signed-off-by: fatpa <fatpa.cai@gmail.com>
This commit is contained in:
parent
3165f4e8c6
commit
5b3dba7a6a
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
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 apisix
|
||||
|
||||
const (
|
||||
GroupName = "apisix.apache.org"
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
// Package v2 is the v2 version of the API.
|
||||
// +groupName=apisix.apache.org
|
||||
package v2
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/openkruise/rollouts/pkg/apis/apisix"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: apisix.GroupName, Version: "v2"}
|
||||
|
||||
// Kind takes an unqualified kind and returns back a Group qualified GroupKind
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to Scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&ApisixRoute{},
|
||||
&ApisixRouteList{},
|
||||
&ApisixUpstream{},
|
||||
&ApisixUpstreamList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:subresource:status
|
||||
// ApisixRoute is used to define the route rules and upstreams for Apache APISIX.
|
||||
type ApisixRoute struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
Spec ApisixRouteSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
|
||||
Status ApisixStatus `json:"status,omitempty" yaml:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixStatus is the status report for Apisix ingress Resources
|
||||
type ApisixStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteSpec is the spec definition for ApisixRouteSpec.
|
||||
type ApisixRouteSpec struct {
|
||||
HTTP []ApisixRouteHTTP `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
Stream []ApisixRouteStream `json:"stream,omitempty" yaml:"stream,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteHTTP represents a single route in for HTTP traffic.
|
||||
type ApisixRouteHTTP struct {
|
||||
// The rule name, cannot be empty.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// Route priority, when multiple routes contains
|
||||
// same URI path (for path matching), route with
|
||||
// higher priority will take effect.
|
||||
Priority int `json:"priority,omitempty" yaml:"priority,omitempty"`
|
||||
Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
Match ApisixRouteHTTPMatch `json:"match,omitempty" yaml:"match,omitempty"`
|
||||
// Backends represents potential backends to proxy after the route
|
||||
// rule matched. When number of backends are more than one, traffic-split
|
||||
// plugin in APISIX will be used to split traffic based on the backend weight.
|
||||
Backends []ApisixRouteHTTPBackend `json:"backends,omitempty" yaml:"backends,omitempty"`
|
||||
Websocket bool `json:"websocket" yaml:"websocket"`
|
||||
PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"`
|
||||
Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
|
||||
Authentication *ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"`
|
||||
}
|
||||
|
||||
// UpstreamTimeout is settings for the read, send and connect to the upstream.
|
||||
type UpstreamTimeout struct {
|
||||
Connect metav1.Duration `json:"connect,omitempty" yaml:"connect,omitempty"`
|
||||
Send metav1.Duration `json:"send,omitempty" yaml:"send,omitempty"`
|
||||
Read metav1.Duration `json:"read,omitempty" yaml:"read,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteHTTPBackend represents a HTTP backend (a Kuberentes Service).
|
||||
type ApisixRouteHTTPBackend struct {
|
||||
// The name (short) of the service, note cross namespace is forbidden,
|
||||
// so be sure the ApisixRoute and Service are in the same namespace.
|
||||
ServiceName string `json:"serviceName" yaml:"serviceName"`
|
||||
// The service port, could be the name or the port number.
|
||||
ServicePort intstr.IntOrString `json:"servicePort" yaml:"servicePort"`
|
||||
// The resolve granularity, can be "endpoints" or "service",
|
||||
// when set to "endpoints", the pod ips will be used; other
|
||||
// wise, the service ClusterIP or ExternalIP will be used,
|
||||
// default is endpoints.
|
||||
ResolveGranularity string `json:"resolveGranularity,omitempty" yaml:"resolveGranularity,omitempty"`
|
||||
// Weight of this backend.
|
||||
Weight *int `json:"weight" yaml:"weight"`
|
||||
// Subset specifies a subset for the target Service. The subset should be pre-defined
|
||||
// in ApisixUpstream about this service.
|
||||
Subset string `json:"subset,omitempty" yaml:"subset,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteHTTPMatch represents the match condition for hitting this route.
|
||||
type ApisixRouteHTTPMatch struct {
|
||||
// URI path predicates, at least one path should be
|
||||
// configured, path could be exact or prefix, for prefix path,
|
||||
// append "*" after it, for instance, "/foo*".
|
||||
Paths []string `json:"paths" yaml:"paths"`
|
||||
// HTTP request method predicates.
|
||||
Methods []string `json:"methods,omitempty" yaml:"methods,omitempty"`
|
||||
// HTTP Host predicates, host can be a wildcard domain or
|
||||
// an exact domain. For wildcard domain, only one generic
|
||||
// level is allowed, for instance, "*.foo.com" is valid but
|
||||
// "*.*.foo.com" is not.
|
||||
Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"`
|
||||
// Remote address predicates, items can be valid IPv4 address
|
||||
// or IPv6 address or CIDR.
|
||||
RemoteAddrs []string `json:"remoteAddrs,omitempty" yaml:"remoteAddrs,omitempty"`
|
||||
// NginxVars represents generic match predicates,
|
||||
// it uses Nginx variable systems, so any predicate
|
||||
// like headers, querystring and etc can be leveraged
|
||||
// here to match the route.
|
||||
// For instance, it can be:
|
||||
// nginxVars:
|
||||
// - subject: "$remote_addr"
|
||||
// op: in
|
||||
// value:
|
||||
// - "127.0.0.1"
|
||||
// - "10.0.5.11"
|
||||
NginxVars []ApisixRouteHTTPMatchExpr `json:"exprs,omitempty" yaml:"exprs,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteHTTPMatchExpr represents a binary route match expression .
|
||||
type ApisixRouteHTTPMatchExpr struct {
|
||||
// Subject is the expression subject, it can
|
||||
// be any string composed by literals and nginx
|
||||
// vars.
|
||||
Subject ApisixRouteHTTPMatchExprSubject `json:"subject" yaml:"subject"`
|
||||
// Op is the operator.
|
||||
Op string `json:"op" yaml:"op"`
|
||||
// Set is an array type object of the expression.
|
||||
// It should be used when the Op is "in" or "not_in";
|
||||
Set []string `json:"set" yaml:"set"`
|
||||
// Value is the normal type object for the expression,
|
||||
// it should be used when the Op is not "in" and "not_in".
|
||||
// Set and Value are exclusive so only of them can be set
|
||||
// in the same time.
|
||||
Value *string `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
// ApisixRouteHTTPMatchExprSubject describes the route match expression subject.
|
||||
type ApisixRouteHTTPMatchExprSubject struct {
|
||||
// The subject scope, can be:
|
||||
// ScopeQuery, ScopeHeader, ScopePath
|
||||
// when subject is ScopePath, Name field
|
||||
// will be ignored.
|
||||
Scope string `json:"scope" yaml:"scope"`
|
||||
// The name of subject.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
}
|
||||
|
||||
// ApisixRoutePlugin represents an APISIX plugin.
|
||||
type ApisixRoutePlugin struct {
|
||||
// The plugin name.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// Whether this plugin is in use, default is true.
|
||||
Enable bool `json:"enable" yaml:"enable"`
|
||||
// Plugin configuration.
|
||||
Config ApisixRoutePluginConfig `json:"config" yaml:"config"`
|
||||
}
|
||||
|
||||
// ApisixRoutePluginConfig is the configuration for
|
||||
// any plugins.
|
||||
type ApisixRoutePluginConfig map[string]interface{}
|
||||
|
||||
// ApisixRouteAuthentication is the authentication-related
|
||||
// configuration in ApisixRoute.
|
||||
type ApisixRouteAuthentication struct {
|
||||
Enable bool `json:"enable" yaml:"enable"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
KeyAuth ApisixRouteAuthenticationKeyAuth `json:"keyAuth,omitempty" yaml:"keyAuth,omitempty"`
|
||||
JwtAuth ApisixRouteAuthenticationJwtAuth `json:"jwtAuth,omitempty" yaml:"jwtAuth,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteAuthenticationKeyAuth is the keyAuth-related
|
||||
// configuration in ApisixRouteAuthentication.
|
||||
type ApisixRouteAuthenticationKeyAuth struct {
|
||||
Header string `json:"header,omitempty" yaml:"header,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteAuthenticationJwtAuth is the jwt auth related
|
||||
// configuration in ApisixRouteAuthentication.
|
||||
type ApisixRouteAuthenticationJwtAuth struct {
|
||||
Header string `json:"header,omitempty" yaml:"header,omitempty"`
|
||||
Query string `json:"query,omitempty" yaml:"query,omitempty"`
|
||||
Cookie string `json:"cookie,omitempty" yaml:"cookie,omitempty"`
|
||||
}
|
||||
|
||||
func (p ApisixRoutePluginConfig) DeepCopyInto(out *ApisixRoutePluginConfig) {
|
||||
b, _ := json.Marshal(&p)
|
||||
_ = json.Unmarshal(b, out)
|
||||
}
|
||||
|
||||
func (p *ApisixRoutePluginConfig) DeepCopy() *ApisixRoutePluginConfig {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRoutePluginConfig)
|
||||
p.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// ApisixRouteStream is the configuration for level 4 route
|
||||
type ApisixRouteStream struct {
|
||||
// The rule name, cannot be empty.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Protocol string `json:"protocol" yaml:"protocol"`
|
||||
Match ApisixRouteStreamMatch `json:"match" yaml:"match"`
|
||||
Backend ApisixRouteStreamBackend `json:"backend" yaml:"backend"`
|
||||
Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixRouteStreamMatch represents the match conditions of stream route.
|
||||
type ApisixRouteStreamMatch struct {
|
||||
// IngressPort represents the port listening on the Ingress proxy server.
|
||||
// It should be pre-defined as APISIX doesn't support dynamic listening.
|
||||
IngressPort int32 `json:"ingressPort" yaml:"ingressPort"`
|
||||
}
|
||||
|
||||
// ApisixRouteStreamBackend represents a TCP backend (a Kubernetes Service).
|
||||
type ApisixRouteStreamBackend struct {
|
||||
// The name (short) of the service, note cross namespace is forbidden,
|
||||
// so be sure the ApisixRoute and Service are in the same namespace.
|
||||
ServiceName string `json:"serviceName" yaml:"serviceName"`
|
||||
// The service port, could be the name or the port number.
|
||||
ServicePort intstr.IntOrString `json:"servicePort" yaml:"servicePort"`
|
||||
// The resolve granularity, can be "endpoints" or "service",
|
||||
// when set to "endpoints", the pod ips will be used; other
|
||||
// wise, the service ClusterIP or ExternalIP will be used,
|
||||
// default is endpoints.
|
||||
ResolveGranularity string `json:"resolveGranularity,omitempty" yaml:"resolveGranularity,omitempty"`
|
||||
// Subset specifies a subset for the target Service. The subset should be pre-defined
|
||||
// in ApisixUpstream about this service.
|
||||
Subset string `json:"subset,omitempty" yaml:"subset,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// ApisixRouteList contains a list of ApisixRoute.
|
||||
type ApisixRouteList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []ApisixRoute `json:"items,omitempty" yaml:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:subresource:status
|
||||
// ApisixUpstream is a decorator for Kubernetes Service, it arms the Service
|
||||
// with rich features like health check, retry policies, load balancer and others.
|
||||
// It's designed to have same name with the Kubernetes Service and can be customized
|
||||
// for individual port.
|
||||
type ApisixUpstream struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
|
||||
Spec *ApisixUpstreamSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
|
||||
Status ApisixStatus `json:"status,omitempty" yaml:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixUpstreamSpec describes the specification of ApisixUpstream.
|
||||
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
|
||||
// +optional
|
||||
ExternalNodes []ApisixUpstreamExternalNode `json:"externalNodes,omitempty" yaml:"externalNodes,omitempty"`
|
||||
|
||||
ApisixUpstreamConfig `json:",inline" yaml:",inline"`
|
||||
|
||||
PortLevelSettings []PortLevelSettings `json:"portLevelSettings,omitempty" yaml:"portLevelSettings,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixUpstreamConfig contains rich features on APISIX Upstream, for instance
|
||||
// load balancer, health check, etc.
|
||||
type ApisixUpstreamConfig struct {
|
||||
// LoadBalancer represents the load balancer configuration for Kubernetes Service.
|
||||
// The default strategy is round robin.
|
||||
// +optional
|
||||
LoadBalancer *LoadBalancer `json:"loadbalancer,omitempty" yaml:"loadbalancer,omitempty"`
|
||||
// The scheme used to talk with the upstream.
|
||||
// Now value can be http, grpc.
|
||||
// +optional
|
||||
Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
|
||||
|
||||
// How many times that the proxy (Apache APISIX) should do when
|
||||
// errors occur (error, timeout or bad http status codes like 500, 502).
|
||||
// +optional
|
||||
Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"`
|
||||
|
||||
// Timeout settings for the read, send and connect to the upstream.
|
||||
// +optional
|
||||
Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
|
||||
// The health check configurations for the upstream.
|
||||
// +optional
|
||||
HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"`
|
||||
|
||||
// Set the client certificate when connecting to TLS upstream.
|
||||
// +optional
|
||||
TLSSecret *ApisixSecret `json:"tlsSecret,omitempty" yaml:"tlsSecret,omitempty"`
|
||||
|
||||
// Subsets groups the service endpoints by their labels. Usually used to differentiate
|
||||
// service versions.
|
||||
// +optional
|
||||
Subsets []ApisixUpstreamSubset `json:"subsets,omitempty" yaml:"subsets,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixUpstreamExternalType is the external service type
|
||||
type ApisixUpstreamExternalType string
|
||||
|
||||
const (
|
||||
// ExternalTypeDomain type is a domain
|
||||
// +k8s:deepcopy-gen=false
|
||||
ExternalTypeDomain ApisixUpstreamExternalType = "Domain"
|
||||
|
||||
// ExternalTypeService type is a K8s ExternalName service
|
||||
// +k8s:deepcopy-gen=false
|
||||
ExternalTypeService ApisixUpstreamExternalType = "Service"
|
||||
)
|
||||
|
||||
// ApisixUpstreamExternalNode is the external node conf
|
||||
type ApisixUpstreamExternalNode struct {
|
||||
Name string `json:"name,omitempty" yaml:"name"`
|
||||
Type ApisixUpstreamExternalType `json:"type,omitempty" yaml:"type"`
|
||||
// +optional
|
||||
Weight *int `json:"weight,omitempty" yaml:"weight"`
|
||||
// Port defines the port of the external node
|
||||
// +optional
|
||||
Port *int `json:"port,omitempty" yaml:"port"`
|
||||
}
|
||||
|
||||
// ApisixUpstreamSubset defines a single endpoints group of one Service.
|
||||
type ApisixUpstreamSubset struct {
|
||||
// Name is the name of subset.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// Labels is the label set of this subset.
|
||||
Labels map[string]string `json:"labels" yaml:"labels"`
|
||||
}
|
||||
|
||||
// PortLevelSettings configures the ApisixUpstreamConfig for each individual port. It inherits
|
||||
// configurations from the outer level (the whole Kubernetes Service) and overrides some of
|
||||
// them if they are set on the port level.
|
||||
type PortLevelSettings struct {
|
||||
ApisixUpstreamConfig `json:",inline" yaml:",inline"`
|
||||
|
||||
// Port is a Kubernetes Service port, it should be already defined.
|
||||
Port int32 `json:"port" yaml:"port"`
|
||||
}
|
||||
|
||||
// LoadBalancer describes the load balancing parameters.
|
||||
type LoadBalancer struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
// The HashOn and Key fields are required when Type is "chash".
|
||||
// HashOn represents the key fetching scope.
|
||||
HashOn string `json:"hashOn,omitempty" yaml:"hashOn,omitempty"`
|
||||
// Key represents the hash key.
|
||||
Key string `json:"key,omitempty" yaml:"key,omitempty"`
|
||||
}
|
||||
|
||||
// HealthCheck describes the upstream health check parameters.
|
||||
type HealthCheck struct {
|
||||
Active *ActiveHealthCheck `json:"active" yaml:"active"`
|
||||
Passive *PassiveHealthCheck `json:"passive,omitempty" yaml:"passive,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"`
|
||||
Port int32 `json:"port,omitempty" yaml:"port,omitempty"`
|
||||
HTTPPath string `json:"httpPath,omitempty" yaml:"httpPath,omitempty"`
|
||||
StrictTLS *bool `json:"strictTLS,omitempty" yaml:"strictTLS,omitempty"`
|
||||
RequestHeaders []string `json:"requestHeaders,omitempty" yaml:"requestHeaders,omitempty"`
|
||||
Healthy *ActiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
|
||||
Unhealthy *ActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
|
||||
}
|
||||
|
||||
// PassiveHealthCheck defines the conditions to judge whether
|
||||
// an upstream node is healthy with the passive manager.
|
||||
type PassiveHealthCheck struct {
|
||||
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
||||
Healthy *PassiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
|
||||
Unhealthy *PassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
|
||||
}
|
||||
|
||||
// ActiveHealthCheckHealthy defines the conditions to judge whether
|
||||
// an upstream node is healthy with the active manner.
|
||||
type ActiveHealthCheckHealthy struct {
|
||||
PassiveHealthCheckHealthy `json:",inline" yaml:",inline"`
|
||||
|
||||
Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
|
||||
}
|
||||
|
||||
// ActiveHealthCheckUnhealthy defines the conditions to judge whether
|
||||
// an upstream node is unhealthy with the active manager.
|
||||
type ActiveHealthCheckUnhealthy struct {
|
||||
PassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"`
|
||||
|
||||
Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
|
||||
}
|
||||
|
||||
// PassiveHealthCheckHealthy defines the conditions to judge whether
|
||||
// an upstream node is healthy with the passive manner.
|
||||
type PassiveHealthCheckHealthy struct {
|
||||
HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ApisixUpstreamList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []ApisixUpstream `json:"items,omitempty" yaml:"items,omitempty"`
|
||||
}
|
||||
|
||||
// ApisixSecret describes the Kubernetes Secret name and namespace.
|
||||
type ApisixSecret struct {
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Required
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Required
|
||||
Namespace string `json:"namespace" yaml:"namespace"`
|
||||
}
|
||||
|
|
@ -0,0 +1,844 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
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 deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRoute) DeepCopyInto(out *ApisixRoute) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoute.
|
||||
func (in *ApisixRoute) DeepCopy() *ApisixRoute {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRoute)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ApisixRoute) 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 *ApisixRouteAuthentication) DeepCopyInto(out *ApisixRouteAuthentication) {
|
||||
*out = *in
|
||||
out.KeyAuth = in.KeyAuth
|
||||
out.JwtAuth = in.JwtAuth
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteAuthentication.
|
||||
func (in *ApisixRouteAuthentication) DeepCopy() *ApisixRouteAuthentication {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteAuthentication)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteAuthenticationJwtAuth) DeepCopyInto(out *ApisixRouteAuthenticationJwtAuth) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteAuthenticationJwtAuth.
|
||||
func (in *ApisixRouteAuthenticationJwtAuth) DeepCopy() *ApisixRouteAuthenticationJwtAuth {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteAuthenticationJwtAuth)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteAuthenticationKeyAuth) DeepCopyInto(out *ApisixRouteAuthenticationKeyAuth) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteAuthenticationKeyAuth.
|
||||
func (in *ApisixRouteAuthenticationKeyAuth) DeepCopy() *ApisixRouteAuthenticationKeyAuth {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteAuthenticationKeyAuth)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteHTTP) DeepCopyInto(out *ApisixRouteHTTP) {
|
||||
*out = *in
|
||||
if in.Timeout != nil {
|
||||
in, out := &in.Timeout, &out.Timeout
|
||||
*out = new(UpstreamTimeout)
|
||||
**out = **in
|
||||
}
|
||||
in.Match.DeepCopyInto(&out.Match)
|
||||
if in.Backends != nil {
|
||||
in, out := &in.Backends, &out.Backends
|
||||
*out = make([]ApisixRouteHTTPBackend, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Plugins != nil {
|
||||
in, out := &in.Plugins, &out.Plugins
|
||||
*out = make([]ApisixRoutePlugin, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Authentication != nil {
|
||||
in, out := &in.Authentication, &out.Authentication
|
||||
*out = new(ApisixRouteAuthentication)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTP.
|
||||
func (in *ApisixRouteHTTP) DeepCopy() *ApisixRouteHTTP {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteHTTP)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteHTTPBackend) DeepCopyInto(out *ApisixRouteHTTPBackend) {
|
||||
*out = *in
|
||||
out.ServicePort = in.ServicePort
|
||||
if in.Weight != nil {
|
||||
in, out := &in.Weight, &out.Weight
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTPBackend.
|
||||
func (in *ApisixRouteHTTPBackend) DeepCopy() *ApisixRouteHTTPBackend {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteHTTPBackend)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteHTTPMatch) DeepCopyInto(out *ApisixRouteHTTPMatch) {
|
||||
*out = *in
|
||||
if in.Paths != nil {
|
||||
in, out := &in.Paths, &out.Paths
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Methods != nil {
|
||||
in, out := &in.Methods, &out.Methods
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Hosts != nil {
|
||||
in, out := &in.Hosts, &out.Hosts
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.RemoteAddrs != nil {
|
||||
in, out := &in.RemoteAddrs, &out.RemoteAddrs
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.NginxVars != nil {
|
||||
in, out := &in.NginxVars, &out.NginxVars
|
||||
*out = make([]ApisixRouteHTTPMatchExpr, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTPMatch.
|
||||
func (in *ApisixRouteHTTPMatch) DeepCopy() *ApisixRouteHTTPMatch {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteHTTPMatch)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteHTTPMatchExpr) DeepCopyInto(out *ApisixRouteHTTPMatchExpr) {
|
||||
*out = *in
|
||||
out.Subject = in.Subject
|
||||
if in.Set != nil {
|
||||
in, out := &in.Set, &out.Set
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Value != nil {
|
||||
in, out := &in.Value, &out.Value
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTPMatchExpr.
|
||||
func (in *ApisixRouteHTTPMatchExpr) DeepCopy() *ApisixRouteHTTPMatchExpr {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteHTTPMatchExpr)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteHTTPMatchExprSubject) DeepCopyInto(out *ApisixRouteHTTPMatchExprSubject) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTPMatchExprSubject.
|
||||
func (in *ApisixRouteHTTPMatchExprSubject) DeepCopy() *ApisixRouteHTTPMatchExprSubject {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteHTTPMatchExprSubject)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteList) DeepCopyInto(out *ApisixRouteList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ApisixRoute, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteList.
|
||||
func (in *ApisixRouteList) DeepCopy() *ApisixRouteList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ApisixRouteList) 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 *ApisixRoutePlugin) DeepCopyInto(out *ApisixRoutePlugin) {
|
||||
*out = *in
|
||||
in.Config.DeepCopyInto(&out.Config)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoutePlugin.
|
||||
func (in *ApisixRoutePlugin) DeepCopy() *ApisixRoutePlugin {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRoutePlugin)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteSpec) DeepCopyInto(out *ApisixRouteSpec) {
|
||||
*out = *in
|
||||
if in.HTTP != nil {
|
||||
in, out := &in.HTTP, &out.HTTP
|
||||
*out = make([]ApisixRouteHTTP, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Stream != nil {
|
||||
in, out := &in.Stream, &out.Stream
|
||||
*out = make([]ApisixRouteStream, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteSpec.
|
||||
func (in *ApisixRouteSpec) DeepCopy() *ApisixRouteSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteStream) DeepCopyInto(out *ApisixRouteStream) {
|
||||
*out = *in
|
||||
out.Match = in.Match
|
||||
out.Backend = in.Backend
|
||||
if in.Plugins != nil {
|
||||
in, out := &in.Plugins, &out.Plugins
|
||||
*out = make([]ApisixRoutePlugin, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteStream.
|
||||
func (in *ApisixRouteStream) DeepCopy() *ApisixRouteStream {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteStream)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteStreamBackend) DeepCopyInto(out *ApisixRouteStreamBackend) {
|
||||
*out = *in
|
||||
out.ServicePort = in.ServicePort
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteStreamBackend.
|
||||
func (in *ApisixRouteStreamBackend) DeepCopy() *ApisixRouteStreamBackend {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteStreamBackend)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixRouteStreamMatch) DeepCopyInto(out *ApisixRouteStreamMatch) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteStreamMatch.
|
||||
func (in *ApisixRouteStreamMatch) DeepCopy() *ApisixRouteStreamMatch {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixRouteStreamMatch)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixStatus) DeepCopyInto(out *ApisixStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixStatus.
|
||||
func (in *ApisixStatus) DeepCopy() *ApisixStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UpstreamTimeout) DeepCopyInto(out *UpstreamTimeout) {
|
||||
*out = *in
|
||||
out.Connect = in.Connect
|
||||
out.Send = in.Send
|
||||
out.Read = in.Read
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamTimeout.
|
||||
func (in *UpstreamTimeout) DeepCopy() *UpstreamTimeout {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UpstreamTimeout)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixUpstream) DeepCopyInto(out *ApisixUpstream) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
if in.Spec != nil {
|
||||
in, out := &in.Spec, &out.Spec
|
||||
*out = new(ApisixUpstreamSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstream.
|
||||
func (in *ApisixUpstream) DeepCopy() *ApisixUpstream {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstream)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ApisixUpstream) 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 *ApisixUpstreamConfig) DeepCopyInto(out *ApisixUpstreamConfig) {
|
||||
*out = *in
|
||||
if in.LoadBalancer != nil {
|
||||
in, out := &in.LoadBalancer, &out.LoadBalancer
|
||||
*out = new(LoadBalancer)
|
||||
**out = **in
|
||||
}
|
||||
if in.Retries != nil {
|
||||
in, out := &in.Retries, &out.Retries
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.Timeout != nil {
|
||||
in, out := &in.Timeout, &out.Timeout
|
||||
*out = new(UpstreamTimeout)
|
||||
**out = **in
|
||||
}
|
||||
if in.HealthCheck != nil {
|
||||
in, out := &in.HealthCheck, &out.HealthCheck
|
||||
*out = new(HealthCheck)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.TLSSecret != nil {
|
||||
in, out := &in.TLSSecret, &out.TLSSecret
|
||||
*out = new(ApisixSecret)
|
||||
**out = **in
|
||||
}
|
||||
if in.Subsets != nil {
|
||||
in, out := &in.Subsets, &out.Subsets
|
||||
*out = make([]ApisixUpstreamSubset, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstreamConfig.
|
||||
func (in *ApisixUpstreamConfig) DeepCopy() *ApisixUpstreamConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstreamConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixUpstreamExternalNode) DeepCopyInto(out *ApisixUpstreamExternalNode) {
|
||||
*out = *in
|
||||
if in.Weight != nil {
|
||||
in, out := &in.Weight, &out.Weight
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
if in.Port != nil {
|
||||
in, out := &in.Port, &out.Port
|
||||
*out = new(int)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstreamExternalNode.
|
||||
func (in *ApisixUpstreamExternalNode) DeepCopy() *ApisixUpstreamExternalNode {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstreamExternalNode)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixUpstreamExternalType) DeepCopyInto(out *ApisixUpstreamExternalType) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstreamExternalType.
|
||||
func (in *ApisixUpstreamExternalType) DeepCopy() *ApisixUpstreamExternalType {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstreamExternalType)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixUpstreamList) DeepCopyInto(out *ApisixUpstreamList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ApisixUpstream, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstreamList.
|
||||
func (in *ApisixUpstreamList) DeepCopy() *ApisixUpstreamList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstreamList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ApisixUpstreamList) 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 *ApisixUpstreamSpec) DeepCopyInto(out *ApisixUpstreamSpec) {
|
||||
*out = *in
|
||||
if in.ExternalNodes != nil {
|
||||
in, out := &in.ExternalNodes, &out.ExternalNodes
|
||||
*out = make([]ApisixUpstreamExternalNode, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.ApisixUpstreamConfig.DeepCopyInto(&out.ApisixUpstreamConfig)
|
||||
if in.PortLevelSettings != nil {
|
||||
in, out := &in.PortLevelSettings, &out.PortLevelSettings
|
||||
*out = make([]PortLevelSettings, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstreamSpec.
|
||||
func (in *ApisixUpstreamSpec) DeepCopy() *ApisixUpstreamSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstreamSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApisixUpstreamSubset) DeepCopyInto(out *ApisixUpstreamSubset) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixUpstreamSubset.
|
||||
func (in *ApisixUpstreamSubset) DeepCopy() *ApisixUpstreamSubset {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApisixUpstreamSubset)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HealthCheck) DeepCopyInto(out *HealthCheck) {
|
||||
*out = *in
|
||||
if in.Active != nil {
|
||||
in, out := &in.Active, &out.Active
|
||||
*out = new(ActiveHealthCheck)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Passive != nil {
|
||||
in, out := &in.Passive, &out.Passive
|
||||
*out = new(PassiveHealthCheck)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheck.
|
||||
func (in *HealthCheck) DeepCopy() *HealthCheck {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HealthCheck)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancer.
|
||||
func (in *LoadBalancer) DeepCopy() *LoadBalancer {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(LoadBalancer)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) {
|
||||
*out = *in
|
||||
if in.Healthy != nil {
|
||||
in, out := &in.Healthy, &out.Healthy
|
||||
*out = new(PassiveHealthCheckHealthy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Unhealthy != nil {
|
||||
in, out := &in.Unhealthy, &out.Unhealthy
|
||||
*out = new(PassiveHealthCheckUnhealthy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheck.
|
||||
func (in *PassiveHealthCheck) DeepCopy() *PassiveHealthCheck {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PassiveHealthCheck)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PassiveHealthCheckHealthy) DeepCopyInto(out *PassiveHealthCheckHealthy) {
|
||||
*out = *in
|
||||
if in.HTTPCodes != nil {
|
||||
in, out := &in.HTTPCodes, &out.HTTPCodes
|
||||
*out = make([]int, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckHealthy.
|
||||
func (in *PassiveHealthCheckHealthy) DeepCopy() *PassiveHealthCheckHealthy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PassiveHealthCheckHealthy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PassiveHealthCheckUnhealthy) DeepCopyInto(out *PassiveHealthCheckUnhealthy) {
|
||||
*out = *in
|
||||
if in.HTTPCodes != nil {
|
||||
in, out := &in.HTTPCodes, &out.HTTPCodes
|
||||
*out = make([]int, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckUnhealthy.
|
||||
func (in *PassiveHealthCheckUnhealthy) DeepCopy() *PassiveHealthCheckUnhealthy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PassiveHealthCheckUnhealthy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PortLevelSettings) DeepCopyInto(out *PortLevelSettings) {
|
||||
*out = *in
|
||||
in.ApisixUpstreamConfig.DeepCopyInto(&out.ApisixUpstreamConfig)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortLevelSettings.
|
||||
func (in *PortLevelSettings) DeepCopy() *PortLevelSettings {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PortLevelSettings)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ActiveHealthCheck) DeepCopyInto(out *ActiveHealthCheck) {
|
||||
*out = *in
|
||||
if in.StrictTLS != nil {
|
||||
in, out := &in.StrictTLS, &out.StrictTLS
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.RequestHeaders != nil {
|
||||
in, out := &in.RequestHeaders, &out.RequestHeaders
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Healthy != nil {
|
||||
in, out := &in.Healthy, &out.Healthy
|
||||
*out = new(ActiveHealthCheckHealthy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Unhealthy != nil {
|
||||
in, out := &in.Unhealthy, &out.Unhealthy
|
||||
*out = new(ActiveHealthCheckUnhealthy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheck.
|
||||
func (in *ActiveHealthCheck) DeepCopy() *ActiveHealthCheck {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ActiveHealthCheck)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ActiveHealthCheckHealthy) DeepCopyInto(out *ActiveHealthCheckHealthy) {
|
||||
*out = *in
|
||||
in.PassiveHealthCheckHealthy.DeepCopyInto(&out.PassiveHealthCheckHealthy)
|
||||
out.Interval = in.Interval
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckHealthy.
|
||||
func (in *ActiveHealthCheckHealthy) DeepCopy() *ActiveHealthCheckHealthy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ActiveHealthCheckHealthy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ActiveHealthCheckUnhealthy) DeepCopyInto(out *ActiveHealthCheckUnhealthy) {
|
||||
*out = *in
|
||||
in.PassiveHealthCheckUnhealthy.DeepCopyInto(&out.PassiveHealthCheckUnhealthy)
|
||||
out.Interval = in.Interval
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckUnhealthy.
|
||||
func (in *ActiveHealthCheckUnhealthy) DeepCopy() *ActiveHealthCheckUnhealthy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ActiveHealthCheckUnhealthy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
@ -223,14 +223,28 @@ func (m *Manager) FinalisingTrafficRouting(c *util.RolloutContext, onlyRestoreSt
|
|||
func newNetworkProvider(c client.Client, rollout *v1alpha1.Rollout, newStatus *v1alpha1.RolloutStatus, sService, cService string) (network.NetworkProvider, error) {
|
||||
trafficRouting := rollout.Spec.Strategy.Canary.TrafficRoutings[0]
|
||||
if trafficRouting.Ingress != nil {
|
||||
return ingress.NewIngressTrafficRouting(c, ingress.Config{
|
||||
RolloutName: rollout.Name,
|
||||
RolloutNs: rollout.Namespace,
|
||||
CanaryService: cService,
|
||||
StableService: sService,
|
||||
TrafficConf: trafficRouting.Ingress,
|
||||
OwnerRef: *metav1.NewControllerRef(rollout, rolloutControllerKind),
|
||||
})
|
||||
switch trafficRouting.Ingress.ClassType {
|
||||
case "apisix":
|
||||
return ingress.NewApisixIngressTrafficRouting(c, ingress.Config{
|
||||
RolloutName: rollout.Name,
|
||||
RolloutNs: rollout.Namespace,
|
||||
CanaryService: cService,
|
||||
StableService: sService,
|
||||
TrafficConf: trafficRouting.Ingress,
|
||||
OwnerRef: *metav1.NewControllerRef(rollout, rolloutControllerKind),
|
||||
})
|
||||
case "nginx":
|
||||
case "alb":
|
||||
default:
|
||||
return ingress.NewNginxIngressTrafficRouting(c, ingress.Config{
|
||||
RolloutName: rollout.Name,
|
||||
RolloutNs: rollout.Namespace,
|
||||
CanaryService: cService,
|
||||
StableService: sService,
|
||||
TrafficConf: trafficRouting.Ingress,
|
||||
OwnerRef: *metav1.NewControllerRef(rollout, rolloutControllerKind),
|
||||
})
|
||||
}
|
||||
}
|
||||
if trafficRouting.Gateway != nil {
|
||||
return gateway.NewGatewayTrafficRouting(c, gateway.Config{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
v1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
a6v2 "github.com/openkruise/rollouts/pkg/apis/apisix/v2"
|
||||
"github.com/openkruise/rollouts/pkg/trafficrouting/network"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type apisixIngressController struct {
|
||||
client.Client
|
||||
conf Config
|
||||
}
|
||||
|
||||
func NewApisixIngressTrafficRouting(client client.Client, conf Config) (network.NetworkProvider, error) {
|
||||
r := &apisixIngressController{
|
||||
Client: client,
|
||||
conf: conf,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *apisixIngressController) Initialize(ctx context.Context) error {
|
||||
apisixRoute := &a6v2.ApisixRoute{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.TrafficConf.Name}, apisixRoute)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) get apisix route(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.TrafficConf.Name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
apisixUpstream := &a6v2.ApisixUpstream{}
|
||||
err = r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.StableService}, apisixUpstream)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) get apisix upstream(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.StableService, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
apisixRoute, err = r.buildCanaryApisixRoute(apisixRoute)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) build canary apisix route failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
canaryApisixUpstream, err := r.buildCanaryApisixUpstream(apisixUpstream)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) build canary apisix upstream failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.Create(ctx, canaryApisixUpstream); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) create canary apisix upstream failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.Update(ctx, apisixRoute); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) update apisix route failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) update apisix route(%s) success", r.conf.RolloutNs, r.conf.RolloutName, util.DumpJSON(apisixRoute))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apisixIngressController) EnsureRoutes(ctx context.Context, weight *int32, matches []v1alpha1.HttpRouteMatch) (bool, error) {
|
||||
if *weight == 0 {
|
||||
return true, fmt.Errorf("rollout(%s/%s) update failed: no valid weights", r.conf.RolloutNs, r.conf.RolloutName)
|
||||
}
|
||||
|
||||
apisixRoute := &a6v2.ApisixRoute{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.TrafficConf.Name}, apisixRoute)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) get apisix route(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.TrafficConf.Name, err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
apisixRouteClone := apisixRoute.DeepCopy()
|
||||
|
||||
targetHTTPRoutes, _ := r.getTargetHTTPRoutes(apisixRouteClone, r.conf.StableService)
|
||||
for index, thr := range targetHTTPRoutes {
|
||||
backends := thr.Backends
|
||||
|
||||
w := int(utilpointer.Int32Deref(weight, 0))
|
||||
for i, backend := range backends {
|
||||
if backend.ServiceName == r.conf.StableService {
|
||||
if backend.Weight == utilpointer.Int(0) {
|
||||
return false, fmt.Errorf("rollout(%s/%s) update failed: no valid stable service backend weights", r.conf.RolloutNs, r.conf.RolloutName)
|
||||
}
|
||||
backends[i].Weight = utilpointer.Int(100 - w)
|
||||
} else if backend.ServiceName == r.conf.CanaryService {
|
||||
backends[i].Weight = utilpointer.Int(w)
|
||||
}
|
||||
}
|
||||
apisixRouteClone.Spec.HTTP[index].Backends = backends
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(apisixRouteClone.Spec, apisixRoute.Spec) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err = r.Update(ctx, apisixRouteClone); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) update apisix route weight failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *apisixIngressController) Finalise(ctx context.Context) error {
|
||||
apisixRoute := &a6v2.ApisixRoute{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.TrafficConf.Name}, apisixRoute)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) get apisix route(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.TrafficConf.Name, err.Error())
|
||||
return err
|
||||
}
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
canaryApisixUpstream := &a6v2.ApisixUpstream{}
|
||||
err = r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.CanaryService}, canaryApisixUpstream)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) get canary apisix upstream(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.CanaryService, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// First, set canary backend 0 weight
|
||||
targetHTTPRoutes, _ := r.getTargetHTTPRoutes(apisixRoute, r.conf.StableService)
|
||||
for index, thr := range targetHTTPRoutes {
|
||||
backends := thr.Backends
|
||||
|
||||
var canaryBackendIndex int
|
||||
var canaryBackendExists bool
|
||||
for i, backend := range backends {
|
||||
if backend.ServiceName == r.conf.StableService {
|
||||
backends[i].Weight = utilpointer.Int(100)
|
||||
} else if backend.ServiceName == r.conf.CanaryService {
|
||||
canaryBackendIndex = i
|
||||
canaryBackendExists = true
|
||||
}
|
||||
}
|
||||
|
||||
if !canaryBackendExists {
|
||||
return fmt.Errorf("rollout(%s/%s) get apisix route canary backend(%s) failed", r.conf.RolloutNs, r.conf.RolloutName, r.conf.CanaryService)
|
||||
}
|
||||
|
||||
// Remove canary backend from backends array by index
|
||||
if len(backends) == canaryBackendIndex+1 {
|
||||
backends = backends[:canaryBackendIndex]
|
||||
} else {
|
||||
backends = append(backends[:canaryBackendIndex], backends[canaryBackendIndex+1:]...)
|
||||
}
|
||||
apisixRoute.Spec.HTTP[index].Backends = backends
|
||||
}
|
||||
|
||||
// Second, update apisix route object and remove canary backend
|
||||
if err = r.Update(ctx, apisixRoute); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) remove apisix route canary backend(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.CanaryService, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) remove apisix route canary backend(%s) success", r.conf.RolloutNs, r.conf.RolloutName, r.conf.CanaryService)
|
||||
|
||||
// Finally, remove canary apisix upstream object
|
||||
if err = r.Delete(ctx, canaryApisixUpstream); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) remove canary apisix upstream(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.conf.CanaryService, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) remove canary apisix upstream(%s) success", r.conf.RolloutNs, r.conf.RolloutName, r.conf.CanaryService)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apisixIngressController) getTargetHTTPRoutes(ar *a6v2.ApisixRoute, serviceName string) (map[int]*a6v2.ApisixRouteHTTP, error) {
|
||||
thr := make(map[int]*a6v2.ApisixRouteHTTP)
|
||||
|
||||
for index, item := range ar.Spec.HTTP {
|
||||
for _, backend := range item.Backends {
|
||||
if backend.ServiceName == serviceName {
|
||||
thr[index] = item.DeepCopy()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(thr) <= 0 {
|
||||
return nil, fmt.Errorf("can not find %s backend on apisix route %s.%s ",
|
||||
serviceName, r.conf.RolloutNs, r.conf.TrafficConf.Name)
|
||||
}
|
||||
|
||||
return thr, nil
|
||||
}
|
||||
|
||||
func (r *apisixIngressController) buildCanaryApisixRoute(ar *a6v2.ApisixRoute) (*a6v2.ApisixRoute, error) {
|
||||
desiredApisixRoute := &a6v2.ApisixRoute{
|
||||
ObjectMeta: *ar.ObjectMeta.DeepCopy(),
|
||||
Spec: a6v2.ApisixRouteSpec{
|
||||
HTTP: ar.Spec.DeepCopy().HTTP,
|
||||
},
|
||||
}
|
||||
|
||||
apisixRouteClone := ar.DeepCopy()
|
||||
if len(apisixRouteClone.Spec.HTTP) == 0 {
|
||||
return nil, fmt.Errorf("apisix route %s.%s's spec.http is empty",
|
||||
r.conf.RolloutNs, r.conf.TrafficConf.Name)
|
||||
}
|
||||
|
||||
targetHTTPRoutes, err := r.getTargetHTTPRoutes(apisixRouteClone, r.conf.StableService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, thr := range targetHTTPRoutes {
|
||||
if len(thr.Backends) != 1 {
|
||||
return nil, fmt.Errorf("apisix route %s.%s's http route %s only one http backend is supported",
|
||||
r.conf.RolloutNs, r.conf.TrafficConf.Name, thr.Name)
|
||||
}
|
||||
|
||||
primaryBackend := thr.Backends[0]
|
||||
primaryBackend.ServiceName = r.conf.StableService
|
||||
primaryWeight := 100
|
||||
primaryBackend.Weight = utilpointer.Int(primaryWeight)
|
||||
thr.Backends[0] = primaryBackend
|
||||
|
||||
canaryWeight := 0
|
||||
canaryBackend := a6v2.ApisixRouteHTTPBackend{
|
||||
ServiceName: r.conf.CanaryService,
|
||||
ServicePort: primaryBackend.ServicePort,
|
||||
ResolveGranularity: primaryBackend.ResolveGranularity,
|
||||
Weight: utilpointer.Int(canaryWeight),
|
||||
}
|
||||
|
||||
desiredApisixRoute.Spec.HTTP[index].Backends = append(thr.Backends, canaryBackend)
|
||||
}
|
||||
|
||||
return desiredApisixRoute, nil
|
||||
}
|
||||
|
||||
func (r *apisixIngressController) buildCanaryApisixUpstream(au *a6v2.ApisixUpstream) (*a6v2.ApisixUpstream, error) {
|
||||
desiredApisixUpstream := &a6v2.ApisixUpstream{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: au.Name,
|
||||
Namespace: au.Namespace,
|
||||
Annotations: au.Annotations,
|
||||
},
|
||||
Spec: au.Spec.DeepCopy(),
|
||||
}
|
||||
desiredApisixUpstream.ObjectMeta.Name = r.conf.CanaryService
|
||||
|
||||
return desiredApisixUpstream, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,433 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file expect 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 ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
v1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
a6v2 "github.com/openkruise/rollouts/pkg/apis/apisix/v2"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
config = Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &v1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
ClassType: "apisix",
|
||||
},
|
||||
}
|
||||
demoApisixRoute = a6v2.ApisixRoute{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "echoserver",
|
||||
},
|
||||
Spec: a6v2.ApisixRouteSpec{
|
||||
HTTP: []a6v2.ApisixRouteHTTP{
|
||||
{
|
||||
Name: "echo",
|
||||
Match: a6v2.ApisixRouteHTTPMatch{
|
||||
Paths: []string{
|
||||
"/apis/echo",
|
||||
},
|
||||
Hosts: []string{
|
||||
"echoserver.example.com",
|
||||
},
|
||||
},
|
||||
Backends: []a6v2.ApisixRouteHTTPBackend{
|
||||
{
|
||||
ServiceName: "echoserver",
|
||||
ServicePort: intstr.IntOrString{Type: 0, IntVal: 80},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "other",
|
||||
Match: a6v2.ApisixRouteHTTPMatch{
|
||||
Paths: []string{
|
||||
"/apis/other",
|
||||
},
|
||||
Hosts: []string{
|
||||
"echoserver.example.com",
|
||||
},
|
||||
},
|
||||
Backends: []a6v2.ApisixRouteHTTPBackend{
|
||||
{
|
||||
ServiceName: "other",
|
||||
ServicePort: intstr.IntOrString{Type: 0, IntVal: 80},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "log",
|
||||
Match: a6v2.ApisixRouteHTTPMatch{
|
||||
Paths: []string{
|
||||
"/apis/logs",
|
||||
},
|
||||
Hosts: []string{
|
||||
"log.example.com",
|
||||
},
|
||||
},
|
||||
Backends: []a6v2.ApisixRouteHTTPBackend{
|
||||
{
|
||||
ServiceName: "echoserver",
|
||||
ServicePort: intstr.IntOrString{Type: 0, IntVal: 8899},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
demoApisixUpstream = a6v2.ApisixUpstream{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "echoserver",
|
||||
},
|
||||
Spec: &a6v2.ApisixUpstreamSpec{
|
||||
ApisixUpstreamConfig: a6v2.ApisixUpstreamConfig{
|
||||
LoadBalancer: &a6v2.LoadBalancer{
|
||||
Type: "roundrobin",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestApisixInitialize(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getApisixRoute func() []*a6v2.ApisixRoute
|
||||
getApisixUpstream func() []*a6v2.ApisixUpstream
|
||||
expectApisixRoute func() *a6v2.ApisixRoute
|
||||
expectApisixUpstream func() *a6v2.ApisixUpstream
|
||||
}{
|
||||
{
|
||||
name: "init apisix test1",
|
||||
getApisixRoute: func() []*a6v2.ApisixRoute {
|
||||
return []*a6v2.ApisixRoute{demoApisixRoute.DeepCopy()}
|
||||
},
|
||||
getApisixUpstream: func() []*a6v2.ApisixUpstream {
|
||||
return []*a6v2.ApisixUpstream{demoApisixUpstream.DeepCopy()}
|
||||
},
|
||||
expectApisixRoute: func() *a6v2.ApisixRoute {
|
||||
expect := demoApisixRoute.DeepCopy()
|
||||
|
||||
expect.Spec.HTTP[0].Backends[0].Weight = utilpointer.Int(100)
|
||||
expect.Spec.HTTP[0].Backends = append(expect.Spec.HTTP[0].Backends, *expect.Spec.HTTP[0].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[0].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[0].Backends[1].Weight = utilpointer.Int(0)
|
||||
|
||||
expect.Spec.HTTP[2].Backends[0].Weight = utilpointer.Int(100)
|
||||
expect.Spec.HTTP[2].Backends = append(expect.Spec.HTTP[2].Backends, *expect.Spec.HTTP[2].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[2].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[2].Backends[1].Weight = utilpointer.Int(0)
|
||||
|
||||
return expect
|
||||
},
|
||||
expectApisixUpstream: func() *a6v2.ApisixUpstream {
|
||||
expect := demoApisixUpstream.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(apisixScheme).Build()
|
||||
for _, route := range cs.getApisixRoute() {
|
||||
fakeCli.Create(context.TODO(), route)
|
||||
}
|
||||
|
||||
for _, upstream := range cs.getApisixUpstream() {
|
||||
fakeCli.Create(context.TODO(), upstream)
|
||||
}
|
||||
|
||||
controller, err := NewApisixIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewApisixIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.Initialize(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
canaryApisixRoute := &a6v2.ApisixRoute{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver"}, canaryApisixRoute)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary apisix route failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
expectApisixRoute := cs.expectApisixRoute()
|
||||
if !reflect.DeepEqual(canaryApisixRoute.Annotations, expectApisixRoute.Annotations) ||
|
||||
!reflect.DeepEqual(canaryApisixRoute.Spec, expectApisixRoute.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expectApisixRoute), util.DumpJSON(canaryApisixRoute))
|
||||
}
|
||||
|
||||
canaryApisixUpstream := &a6v2.ApisixUpstream{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryApisixUpstream)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary apisix upstream failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
expectApisixUpstream := cs.expectApisixUpstream()
|
||||
if !reflect.DeepEqual(canaryApisixUpstream.Annotations, expectApisixUpstream.Annotations) ||
|
||||
!reflect.DeepEqual(canaryApisixUpstream.Spec, expectApisixUpstream.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expectApisixUpstream), util.DumpJSON(canaryApisixUpstream))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApisixEnsureRoutes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getApisixRoute func() []*a6v2.ApisixRoute
|
||||
getApisixUpstream func() []*a6v2.ApisixUpstream
|
||||
getRoutes func() (*int32, []v1alpha1.HttpRouteMatch)
|
||||
expectApisixRoute func() *a6v2.ApisixRoute
|
||||
ingressType string
|
||||
}{
|
||||
{
|
||||
name: "ensure apisix routes test1 with 0 weight",
|
||||
getApisixRoute: func() []*a6v2.ApisixRoute {
|
||||
return []*a6v2.ApisixRoute{demoApisixRoute.DeepCopy()}
|
||||
},
|
||||
getApisixUpstream: func() []*a6v2.ApisixUpstream {
|
||||
return []*a6v2.ApisixUpstream{demoApisixUpstream.DeepCopy()}
|
||||
},
|
||||
getRoutes: func() (*int32, []v1alpha1.HttpRouteMatch) {
|
||||
return utilpointer.Int32(0), nil
|
||||
},
|
||||
expectApisixRoute: func() *a6v2.ApisixRoute {
|
||||
expect := demoApisixRoute.DeepCopy()
|
||||
|
||||
expect.Spec.HTTP[0].Backends[0].Weight = utilpointer.Int(100)
|
||||
expect.Spec.HTTP[0].Backends = append(expect.Spec.HTTP[0].Backends, *expect.Spec.HTTP[0].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[0].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[0].Backends[1].Weight = utilpointer.Int(0)
|
||||
|
||||
expect.Spec.HTTP[2].Backends[0].Weight = utilpointer.Int(100)
|
||||
expect.Spec.HTTP[2].Backends = append(expect.Spec.HTTP[2].Backends, *expect.Spec.HTTP[2].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[2].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[2].Backends[1].Weight = utilpointer.Int(0)
|
||||
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure apisix routes test1 with 10 weight",
|
||||
getApisixRoute: func() []*a6v2.ApisixRoute {
|
||||
return []*a6v2.ApisixRoute{demoApisixRoute.DeepCopy()}
|
||||
},
|
||||
getApisixUpstream: func() []*a6v2.ApisixUpstream {
|
||||
return []*a6v2.ApisixUpstream{demoApisixUpstream.DeepCopy()}
|
||||
},
|
||||
getRoutes: func() (*int32, []v1alpha1.HttpRouteMatch) {
|
||||
return utilpointer.Int32(10), nil
|
||||
},
|
||||
expectApisixRoute: func() *a6v2.ApisixRoute {
|
||||
expect := demoApisixRoute.DeepCopy()
|
||||
|
||||
expect.Spec.HTTP[0].Backends[0].Weight = utilpointer.Int(90)
|
||||
expect.Spec.HTTP[0].Backends = append(expect.Spec.HTTP[0].Backends, *expect.Spec.HTTP[0].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[0].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[0].Backends[1].Weight = utilpointer.Int(10)
|
||||
|
||||
expect.Spec.HTTP[2].Backends[0].Weight = utilpointer.Int(90)
|
||||
expect.Spec.HTTP[2].Backends = append(expect.Spec.HTTP[2].Backends, *expect.Spec.HTTP[2].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[2].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[2].Backends[1].Weight = utilpointer.Int(10)
|
||||
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure apisix routes test1 with 100 weight",
|
||||
getApisixRoute: func() []*a6v2.ApisixRoute {
|
||||
return []*a6v2.ApisixRoute{demoApisixRoute.DeepCopy()}
|
||||
},
|
||||
getApisixUpstream: func() []*a6v2.ApisixUpstream {
|
||||
return []*a6v2.ApisixUpstream{demoApisixUpstream.DeepCopy()}
|
||||
},
|
||||
getRoutes: func() (*int32, []v1alpha1.HttpRouteMatch) {
|
||||
return utilpointer.Int32(100), nil
|
||||
},
|
||||
expectApisixRoute: func() *a6v2.ApisixRoute {
|
||||
expect := demoApisixRoute.DeepCopy()
|
||||
|
||||
expect.Spec.HTTP[0].Backends[0].Weight = utilpointer.Int(0)
|
||||
expect.Spec.HTTP[0].Backends = append(expect.Spec.HTTP[0].Backends, *expect.Spec.HTTP[0].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[0].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[0].Backends[1].Weight = utilpointer.Int(100)
|
||||
|
||||
expect.Spec.HTTP[2].Backends[0].Weight = utilpointer.Int(0)
|
||||
expect.Spec.HTTP[2].Backends = append(expect.Spec.HTTP[2].Backends, *expect.Spec.HTTP[2].Backends[0].DeepCopy())
|
||||
expect.Spec.HTTP[2].Backends[1].ServiceName = "echoserver-canary"
|
||||
expect.Spec.HTTP[2].Backends[1].Weight = utilpointer.Int(100)
|
||||
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(apisixScheme).Build()
|
||||
for _, route := range cs.getApisixRoute() {
|
||||
fakeCli.Create(context.TODO(), route)
|
||||
}
|
||||
|
||||
for _, upstream := range cs.getApisixUpstream() {
|
||||
fakeCli.Create(context.TODO(), upstream)
|
||||
}
|
||||
|
||||
controller, err := NewApisixIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewApisixIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.Initialize(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
weight, matches := cs.getRoutes()
|
||||
result, err := controller.EnsureRoutes(context.TODO(), weight, matches)
|
||||
if result {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureRoutes failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
canaryApisixRoute := &a6v2.ApisixRoute{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver"}, canaryApisixRoute)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary apisix route failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
expectApisixRoute := cs.expectApisixRoute()
|
||||
if !reflect.DeepEqual(canaryApisixRoute.Annotations, expectApisixRoute.Annotations) ||
|
||||
!reflect.DeepEqual(canaryApisixRoute.Spec, expectApisixRoute.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expectApisixRoute), util.DumpJSON(canaryApisixRoute))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApisixFinalise(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getApisixRoute func() []*a6v2.ApisixRoute
|
||||
getApisixUpstream func() []*a6v2.ApisixUpstream
|
||||
expectApisixRoute func() *a6v2.ApisixRoute
|
||||
expectApisixUpstream func() *a6v2.ApisixUpstream
|
||||
}{
|
||||
{
|
||||
name: "finalise apisix routes test",
|
||||
getApisixRoute: func() []*a6v2.ApisixRoute {
|
||||
return []*a6v2.ApisixRoute{demoApisixRoute.DeepCopy()}
|
||||
},
|
||||
getApisixUpstream: func() []*a6v2.ApisixUpstream {
|
||||
return []*a6v2.ApisixUpstream{demoApisixUpstream.DeepCopy()}
|
||||
},
|
||||
expectApisixRoute: func() *a6v2.ApisixRoute {
|
||||
expect := demoApisixRoute.DeepCopy()
|
||||
expect.Spec.HTTP[0].Backends[0].Weight = utilpointer.Int(100)
|
||||
expect.Spec.HTTP[2].Backends[0].Weight = utilpointer.Int(100)
|
||||
return expect
|
||||
},
|
||||
expectApisixUpstream: func() *a6v2.ApisixUpstream {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(apisixScheme).Build()
|
||||
for _, route := range cs.getApisixRoute() {
|
||||
fakeCli.Create(context.TODO(), route)
|
||||
}
|
||||
|
||||
for _, upstream := range cs.getApisixUpstream() {
|
||||
fakeCli.Create(context.TODO(), upstream)
|
||||
}
|
||||
|
||||
controller, err := NewApisixIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewApisixIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.Initialize(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = controller.Finalise(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("Finalise failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
canaryApisixRoute := &a6v2.ApisixRoute{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver"}, canaryApisixRoute)
|
||||
if err != nil {
|
||||
if cs.expectApisixRoute() == nil && errors.IsNotFound(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Get canary apisix route failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
expectApisixRoute := cs.expectApisixRoute()
|
||||
if !reflect.DeepEqual(canaryApisixRoute.Annotations, expectApisixRoute.Annotations) ||
|
||||
!reflect.DeepEqual(canaryApisixRoute.Spec, expectApisixRoute.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expectApisixRoute), util.DumpJSON(canaryApisixRoute))
|
||||
}
|
||||
|
||||
canaryApisixUpstream := &a6v2.ApisixUpstream{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryApisixUpstream)
|
||||
if err != nil {
|
||||
if cs.expectApisixUpstream() == nil && errors.IsNotFound(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Get canary apisix upstream failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -17,38 +17,10 @@ limitations under the License.
|
|||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/trafficrouting/network"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"github.com/openkruise/rollouts/pkg/util/configuration"
|
||||
"github.com/openkruise/rollouts/pkg/util/luamanager"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/klog/v2"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type ingressController struct {
|
||||
client.Client
|
||||
conf Config
|
||||
luaManager *luamanager.LuaManager
|
||||
canaryIngressName string
|
||||
luaScript string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RolloutName string
|
||||
RolloutNs string
|
||||
|
|
@ -57,221 +29,3 @@ type Config struct {
|
|||
TrafficConf *rolloutv1alpha1.IngressTrafficRouting
|
||||
OwnerRef metav1.OwnerReference
|
||||
}
|
||||
|
||||
func NewIngressTrafficRouting(client client.Client, conf Config) (network.NetworkProvider, error) {
|
||||
r := &ingressController{
|
||||
Client: client,
|
||||
conf: conf,
|
||||
canaryIngressName: defaultCanaryIngressName(conf.TrafficConf.Name),
|
||||
luaManager: &luamanager.LuaManager{},
|
||||
}
|
||||
luaScript, err := r.getTrafficRoutingIngressLuaScript(conf.TrafficConf.ClassType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.luaScript = luaScript
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Initialize verify the existence of the ingress resource and generate the canary ingress
|
||||
func (r *ingressController) Initialize(ctx context.Context) error {
|
||||
ingress := &netv1.Ingress{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.TrafficConf.Name}, ingress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.canaryIngressName}, canaryIngress)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
klog.Errorf("rollout(%s/%s) get canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.canaryIngressName, err.Error())
|
||||
return err
|
||||
} else if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// build and create canary ingress
|
||||
canaryIngress = r.buildCanaryIngress(ingress)
|
||||
canaryIngress.Annotations, err = r.executeLuaForCanary(canaryIngress.Annotations, utilpointer.Int32(0), nil)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) execute lua failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
if err = r.Create(ctx, canaryIngress); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) create canary ingress failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) create canary ingress(%s) success", r.conf.RolloutNs, r.conf.RolloutName, util.DumpJSON(canaryIngress))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ingressController) EnsureRoutes(ctx context.Context, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (bool, error) {
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: defaultCanaryIngressName(r.conf.TrafficConf.Name)}, canaryIngress)
|
||||
if err != nil {
|
||||
if weight != nil && *weight == 0 && errors.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
klog.Errorf("rollout(%s/%s) get canary ingress failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
newAnnotations, err := r.executeLuaForCanary(canaryIngress.Annotations, weight, matches)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) execute lua failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
if reflect.DeepEqual(canaryIngress.Annotations, newAnnotations) {
|
||||
return true, nil
|
||||
}
|
||||
byte1, _ := json.Marshal(metav1.ObjectMeta{Annotations: canaryIngress.Annotations})
|
||||
byte2, _ := json.Marshal(metav1.ObjectMeta{Annotations: newAnnotations})
|
||||
patch, err := jsonpatch.CreateMergePatch(byte1, byte2)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) create merge patch failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
body := fmt.Sprintf(`{"metadata":%s}`, string(patch))
|
||||
if err = r.Patch(ctx, canaryIngress, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) set canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name, err.Error())
|
||||
return false, err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) set canary ingress(%s) annotations(%s) success", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name, util.DumpJSON(newAnnotations))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *ingressController) Finalise(ctx context.Context) error {
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.canaryIngressName}, canaryIngress)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
klog.Errorf("rollout(%s/%s) get canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.canaryIngressName, err.Error())
|
||||
return err
|
||||
}
|
||||
if errors.IsNotFound(err) || !canaryIngress.DeletionTimestamp.IsZero() {
|
||||
return nil
|
||||
}
|
||||
// immediate delete canary ingress
|
||||
if err = r.Delete(ctx, canaryIngress); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) remove canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) remove canary ingress(%s) success", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ingressController) buildCanaryIngress(stableIngress *netv1.Ingress) *netv1.Ingress {
|
||||
desiredCanaryIngress := &netv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: r.canaryIngressName,
|
||||
Namespace: stableIngress.Namespace,
|
||||
Annotations: stableIngress.Annotations,
|
||||
Labels: stableIngress.Labels,
|
||||
},
|
||||
Spec: netv1.IngressSpec{
|
||||
Rules: make([]netv1.IngressRule, 0),
|
||||
IngressClassName: stableIngress.Spec.IngressClassName,
|
||||
TLS: stableIngress.Spec.TLS,
|
||||
},
|
||||
}
|
||||
hosts := sets.NewString()
|
||||
// Ensure canaryIngress is owned by this Rollout for cleanup
|
||||
desiredCanaryIngress.SetOwnerReferences([]metav1.OwnerReference{r.conf.OwnerRef})
|
||||
// Copy only the rules which reference the stableService from the stableIngress to the canaryIngress
|
||||
// and change service backend to canaryService. Rules **not** referencing the stableIngress will be ignored.
|
||||
for ir := 0; ir < len(stableIngress.Spec.Rules); ir++ {
|
||||
var hasStableServiceBackendRule bool
|
||||
stableRule := stableIngress.Spec.Rules[ir]
|
||||
canaryRule := netv1.IngressRule{
|
||||
Host: stableRule.Host,
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{},
|
||||
},
|
||||
}
|
||||
// Update all backends pointing to the stableService to point to the canaryService now
|
||||
for ip := 0; ip < len(stableRule.HTTP.Paths); ip++ {
|
||||
if stableRule.HTTP.Paths[ip].Backend.Service.Name == r.conf.StableService {
|
||||
hasStableServiceBackendRule = true
|
||||
if stableRule.Host != "" {
|
||||
hosts.Insert(stableRule.Host)
|
||||
}
|
||||
canaryPath := netv1.HTTPIngressPath{
|
||||
Path: stableRule.HTTP.Paths[ip].Path,
|
||||
PathType: stableRule.HTTP.Paths[ip].PathType,
|
||||
Backend: stableRule.HTTP.Paths[ip].Backend,
|
||||
}
|
||||
canaryPath.Backend.Service.Name = r.conf.CanaryService
|
||||
canaryRule.HTTP.Paths = append(canaryRule.HTTP.Paths, canaryPath)
|
||||
}
|
||||
}
|
||||
// If this rule was using the specified stableService backend, append it to the canary Ingress spec
|
||||
if hasStableServiceBackendRule {
|
||||
desiredCanaryIngress.Spec.Rules = append(desiredCanaryIngress.Spec.Rules, canaryRule)
|
||||
}
|
||||
}
|
||||
|
||||
return desiredCanaryIngress
|
||||
}
|
||||
|
||||
func defaultCanaryIngressName(name string) string {
|
||||
return fmt.Sprintf("%s-canary", name)
|
||||
}
|
||||
|
||||
func (r *ingressController) executeLuaForCanary(annotations map[string]string, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (map[string]string, error) {
|
||||
if weight == nil {
|
||||
// the lua script does not have a pointer type,
|
||||
// so we need to pass weight=-1 to indicate the case where weight is nil.
|
||||
weight = utilpointer.Int32(-1)
|
||||
}
|
||||
type LuaData struct {
|
||||
Annotations map[string]string
|
||||
Weight string
|
||||
Matches []rolloutv1alpha1.HttpRouteMatch
|
||||
CanaryService string
|
||||
}
|
||||
data := &LuaData{
|
||||
Annotations: annotations,
|
||||
Weight: fmt.Sprintf("%d", *weight),
|
||||
Matches: matches,
|
||||
CanaryService: r.conf.CanaryService,
|
||||
}
|
||||
unObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj := &unstructured.Unstructured{Object: unObj}
|
||||
l, err := r.luaManager.RunLuaScript(obj, r.luaScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
returnValue := l.Get(-1)
|
||||
if returnValue.Type() == lua.LTTable {
|
||||
jsonBytes, err := luamanager.Encode(returnValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newAnnotations := map[string]string{}
|
||||
err = json.Unmarshal(jsonBytes, &newAnnotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newAnnotations, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expect table output from Lua script, not %s", returnValue.Type().String())
|
||||
}
|
||||
|
||||
func (r *ingressController) getTrafficRoutingIngressLuaScript(iType string) (string, error) {
|
||||
if iType == "" {
|
||||
iType = "nginx"
|
||||
}
|
||||
luaScript, err := configuration.GetTrafficRoutingIngressLuaScript(r.Client, iType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if luaScript != "" {
|
||||
return luaScript, nil
|
||||
}
|
||||
key := fmt.Sprintf("lua_configuration/trafficrouting_ingress/%s.lua", iType)
|
||||
script := util.GetLuaConfigurationContent(key)
|
||||
if script == "" {
|
||||
return "", fmt.Errorf("%s lua script is not found", iType)
|
||||
}
|
||||
return script, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
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.
|
||||
|
|
@ -14,593 +17,25 @@ limitations under the License.
|
|||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rolloutsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"github.com/openkruise/rollouts/pkg/util/configuration"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
a6v2 "github.com/openkruise/rollouts/pkg/apis/apisix/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme *runtime.Scheme
|
||||
|
||||
demoConf = corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configuration.RolloutConfigurationName,
|
||||
Namespace: util.GetRolloutNamespace(),
|
||||
},
|
||||
Data: map[string]string{
|
||||
fmt.Sprintf("%s.nginx", configuration.LuaTrafficRoutingIngressTypePrefix): `
|
||||
annotations = obj.annotations
|
||||
annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-weight"] = nil
|
||||
if ( obj.weight ~= "-1" )
|
||||
then
|
||||
annotations["nginx.ingress.kubernetes.io/canary-weight"] = obj.weight
|
||||
end
|
||||
if ( not obj.matches )
|
||||
then
|
||||
return annotations
|
||||
end
|
||||
for _,match in ipairs(obj.matches) do
|
||||
header = match.headers[1]
|
||||
if ( header.name == "canary-by-cookie" )
|
||||
then
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = header.value
|
||||
else
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header"] = header.name
|
||||
if ( header.type == "RegularExpression" )
|
||||
then
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = header.value
|
||||
else
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = header.value
|
||||
end
|
||||
end
|
||||
end
|
||||
return annotations
|
||||
`,
|
||||
fmt.Sprintf("%s.aliyun-alb", configuration.LuaTrafficRoutingIngressTypePrefix): `
|
||||
function split(input, delimiter)
|
||||
local arr = {}
|
||||
string.gsub(input, '[^' .. delimiter ..']+', function(w) table.insert(arr, w) end)
|
||||
return arr
|
||||
end
|
||||
annotations = obj.annotations
|
||||
annotations["alb.ingress.kubernetes.io/canary"] = "true"
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-cookie"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-header"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-header-pattern"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-header-value"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-weight"] = nil
|
||||
conditionKey = string.format("alb.ingress.kubernetes.io/conditions.%s", obj.canaryService)
|
||||
annotations[conditionKey] = nil
|
||||
if ( obj.weight ~= "-1" )
|
||||
then
|
||||
annotations["alb.ingress.kubernetes.io/canary-weight"] = obj.weight
|
||||
end
|
||||
if ( not obj.matches )
|
||||
then
|
||||
return annotations
|
||||
end
|
||||
if ( annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] )
|
||||
then
|
||||
protocolobj = json.decode(annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"])
|
||||
newprotocolobj = {}
|
||||
for _, v in pairs(protocolobj) do
|
||||
newprotocolobj[obj.canaryService] = v
|
||||
end
|
||||
annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] = json.encode(newprotocolobj)
|
||||
end
|
||||
|
||||
conditions = {}
|
||||
match = obj.matches[1]
|
||||
for _,header in ipairs(match.headers) do
|
||||
condition = {}
|
||||
if ( header.name == "Cookie" )
|
||||
then
|
||||
condition.type = "Cookie"
|
||||
condition.cookieConfig = {}
|
||||
cookies = split(header.value, ";")
|
||||
values = {}
|
||||
for _,cookieStr in ipairs(cookies) do
|
||||
cookie = split(cookieStr, "=")
|
||||
value = {}
|
||||
value.key = cookie[1]
|
||||
value.value = cookie[2]
|
||||
table.insert(values, value)
|
||||
end
|
||||
condition.cookieConfig.values = values
|
||||
elseif ( header.name == "SourceIp" )
|
||||
then
|
||||
condition.type = "SourceIp"
|
||||
condition.sourceIpConfig = {}
|
||||
ips = split(header.value, ";")
|
||||
values = {}
|
||||
for _,ip in ipairs(ips) do
|
||||
table.insert(values, ip)
|
||||
end
|
||||
condition.sourceIpConfig.values = values
|
||||
else
|
||||
condition.type = "Header"
|
||||
condition.headerConfig = {}
|
||||
condition.headerConfig.key = header.name
|
||||
vals = split(header.value, ";")
|
||||
values = {}
|
||||
for _,val in ipairs(vals) do
|
||||
table.insert(values, val)
|
||||
end
|
||||
condition.headerConfig.values = values
|
||||
end
|
||||
table.insert(conditions, condition)
|
||||
end
|
||||
annotations[conditionKey] = json.encode(conditions)
|
||||
return annotations
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
demoIngress = netv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "echoserver",
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "nginx",
|
||||
},
|
||||
},
|
||||
Spec: netv1.IngressSpec{
|
||||
IngressClassName: utilpointer.String("nginx"),
|
||||
TLS: []netv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"echoserver.example.com"},
|
||||
SecretName: "echoserver-name",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"log.example.com"},
|
||||
SecretName: "log-name",
|
||||
},
|
||||
},
|
||||
Rules: []netv1.IngressRule{
|
||||
{
|
||||
Host: "echoserver.example.com",
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{
|
||||
Paths: []netv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/apis/echo",
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: "echoserver",
|
||||
Port: netv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/apis/other",
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: "other",
|
||||
Port: netv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "log.example.com",
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{
|
||||
Paths: []netv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/apis/logs",
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: "echoserver",
|
||||
Port: netv1.ServiceBackendPort{
|
||||
Number: 8899,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
scheme *runtime.Scheme
|
||||
apisixScheme *runtime.Scheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
scheme = runtime.NewScheme()
|
||||
_ = clientgoscheme.AddToScheme(scheme)
|
||||
_ = rolloutsv1alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getConfigmap func() *corev1.ConfigMap
|
||||
getIngress func() []*netv1.Ingress
|
||||
expectIngress func() *netv1.Ingress
|
||||
}{
|
||||
{
|
||||
name: "init test1",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy()}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
},
|
||||
}
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
fakeCli.Create(context.TODO(), cs.getConfigmap())
|
||||
for _, ingress := range cs.getIngress() {
|
||||
fakeCli.Create(context.TODO(), ingress)
|
||||
}
|
||||
controller, err := NewIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
err = controller.Initialize(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryIngress)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary ingress failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
expect := cs.expectIngress()
|
||||
if !reflect.DeepEqual(canaryIngress.Annotations, expect.Annotations) ||
|
||||
!reflect.DeepEqual(canaryIngress.Spec, expect.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect), util.DumpJSON(canaryIngress))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRoutes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getConfigmap func() *corev1.ConfigMap
|
||||
getIngress func() []*netv1.Ingress
|
||||
getRoutes func() (*int32, []rolloutsv1alpha1.HttpRouteMatch)
|
||||
expectIngress func() *netv1.Ingress
|
||||
ingressType string
|
||||
}{
|
||||
{
|
||||
name: "ensure routes test1",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
return nil, []rolloutsv1alpha1.HttpRouteMatch{
|
||||
// header
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "user_id",
|
||||
Value: "123456",
|
||||
},
|
||||
},
|
||||
},
|
||||
// cookies
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "canary-by-cookie",
|
||||
Value: "demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure routes test2",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
return utilpointer.Int32(40), nil
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "40"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure routes test3",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
iType := gatewayv1alpha2.HeaderMatchRegularExpression
|
||||
return nil, []rolloutsv1alpha1.HttpRouteMatch{
|
||||
// header
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "user_id",
|
||||
Value: "123*",
|
||||
Type: &iType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = "123*"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure routes test4",
|
||||
ingressType: "aliyun-alb",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["alb.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["alb.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
canary.Annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] = `{"echoserver":"http"}`
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
return nil, []rolloutsv1alpha1.HttpRouteMatch{
|
||||
// header
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "Cookie",
|
||||
Value: "demo1=value1;demo2=value2",
|
||||
},
|
||||
{
|
||||
Name: "SourceIp",
|
||||
Value: "192.168.0.0/16;172.16.0.0/16",
|
||||
},
|
||||
{
|
||||
Name: "headername",
|
||||
Value: "headervalue1;headervalue2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["alb.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] = `{"echoserver-canary":"http"}`
|
||||
expect.Annotations["alb.ingress.kubernetes.io/conditions.echoserver-canary"] = `[{"cookieConfig":{"values":[{"key":"demo1","value":"value1"},{"key":"demo2","value":"value2"}]},"type":"Cookie"},{"sourceIpConfig":{"values":["192.168.0.0/16","172.16.0.0/16"]},"type":"SourceIp"},{"headerConfig":{"key":"headername","values":["headervalue1","headervalue2"]},"type":"Header"}]`
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
},
|
||||
}
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
fakeCli.Create(context.TODO(), cs.getConfigmap())
|
||||
for _, ingress := range cs.getIngress() {
|
||||
fakeCli.Create(context.TODO(), ingress)
|
||||
}
|
||||
config.TrafficConf.ClassType = cs.ingressType
|
||||
controller, err := NewIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
weight, matches := cs.getRoutes()
|
||||
_, err = controller.EnsureRoutes(context.TODO(), weight, matches)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureRoutes failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryIngress)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary ingress failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
expect := cs.expectIngress()
|
||||
if !reflect.DeepEqual(canaryIngress.Annotations, expect.Annotations) ||
|
||||
!reflect.DeepEqual(canaryIngress.Spec, expect.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect), util.DumpJSON(canaryIngress))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalise(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getConfigmap func() *corev1.ConfigMap
|
||||
getIngress func() []*netv1.Ingress
|
||||
expectIngress func() *netv1.Ingress
|
||||
}{
|
||||
{
|
||||
name: "finalise test1",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
},
|
||||
}
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
fakeCli.Create(context.TODO(), cs.getConfigmap())
|
||||
for _, ingress := range cs.getIngress() {
|
||||
fakeCli.Create(context.TODO(), ingress)
|
||||
}
|
||||
controller, err := NewIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
err = controller.Finalise(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureRoutes failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryIngress)
|
||||
if err != nil {
|
||||
if cs.expectIngress() == nil && errors.IsNotFound(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Get canary ingress failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
expect := cs.expectIngress()
|
||||
if !reflect.DeepEqual(canaryIngress.Annotations, expect.Annotations) ||
|
||||
!reflect.DeepEqual(canaryIngress.Spec, expect.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect), util.DumpJSON(canaryIngress))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
apisixScheme = runtime.NewScheme()
|
||||
_ = a6v2.AddToScheme(apisixScheme)
|
||||
_ = clientgoscheme.AddToScheme(apisixScheme)
|
||||
_ = rolloutsv1alpha1.AddToScheme(apisixScheme)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/trafficrouting/network"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"github.com/openkruise/rollouts/pkg/util/configuration"
|
||||
"github.com/openkruise/rollouts/pkg/util/luamanager"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/klog/v2"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type ingressController struct {
|
||||
client.Client
|
||||
conf Config
|
||||
luaManager *luamanager.LuaManager
|
||||
canaryIngressName string
|
||||
luaScript string
|
||||
}
|
||||
|
||||
func NewNginxIngressTrafficRouting(client client.Client, conf Config) (network.NetworkProvider, error) {
|
||||
r := &ingressController{
|
||||
Client: client,
|
||||
conf: conf,
|
||||
canaryIngressName: defaultCanaryIngressName(conf.TrafficConf.Name),
|
||||
luaManager: &luamanager.LuaManager{},
|
||||
}
|
||||
luaScript, err := r.getTrafficRoutingIngressLuaScript(conf.TrafficConf.ClassType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.luaScript = luaScript
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Initialize verify the existence of the ingress resource and generate the canary ingress
|
||||
func (r *ingressController) Initialize(ctx context.Context) error {
|
||||
ingress := &netv1.Ingress{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.conf.TrafficConf.Name}, ingress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.canaryIngressName}, canaryIngress)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
klog.Errorf("rollout(%s/%s) get canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.canaryIngressName, err.Error())
|
||||
return err
|
||||
} else if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// build and create canary ingress
|
||||
canaryIngress = r.buildCanaryIngress(ingress)
|
||||
canaryIngress.Annotations, err = r.executeLuaForCanary(canaryIngress.Annotations, utilpointer.Int32(0), nil)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) execute lua failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
if err = r.Create(ctx, canaryIngress); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) create canary ingress failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) create canary ingress(%s) success", r.conf.RolloutNs, r.conf.RolloutName, util.DumpJSON(canaryIngress))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ingressController) EnsureRoutes(ctx context.Context, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (bool, error) {
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: defaultCanaryIngressName(r.conf.TrafficConf.Name)}, canaryIngress)
|
||||
if err != nil {
|
||||
if weight != nil && *weight == 0 && errors.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
klog.Errorf("rollout(%s/%s) get canary ingress failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
newAnnotations, err := r.executeLuaForCanary(canaryIngress.Annotations, weight, matches)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) execute lua failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
if reflect.DeepEqual(canaryIngress.Annotations, newAnnotations) {
|
||||
return true, nil
|
||||
}
|
||||
byte1, _ := json.Marshal(metav1.ObjectMeta{Annotations: canaryIngress.Annotations})
|
||||
byte2, _ := json.Marshal(metav1.ObjectMeta{Annotations: newAnnotations})
|
||||
patch, err := jsonpatch.CreateMergePatch(byte1, byte2)
|
||||
if err != nil {
|
||||
klog.Errorf("rollout(%s/%s) create merge patch failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
|
||||
return false, err
|
||||
}
|
||||
body := fmt.Sprintf(`{"metadata":%s}`, string(patch))
|
||||
if err = r.Patch(ctx, canaryIngress, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) set canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name, err.Error())
|
||||
return false, err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) set canary ingress(%s) annotations(%s) success", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name, util.DumpJSON(newAnnotations))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *ingressController) Finalise(ctx context.Context) error {
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: r.canaryIngressName}, canaryIngress)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
klog.Errorf("rollout(%s/%s) get canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, r.canaryIngressName, err.Error())
|
||||
return err
|
||||
}
|
||||
if errors.IsNotFound(err) || !canaryIngress.DeletionTimestamp.IsZero() {
|
||||
return nil
|
||||
}
|
||||
// immediate delete canary ingress
|
||||
if err = r.Delete(ctx, canaryIngress); err != nil {
|
||||
klog.Errorf("rollout(%s/%s) remove canary ingress(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name, err.Error())
|
||||
return err
|
||||
}
|
||||
klog.Infof("rollout(%s/%s) remove canary ingress(%s) success", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ingressController) buildCanaryIngress(stableIngress *netv1.Ingress) *netv1.Ingress {
|
||||
desiredCanaryIngress := &netv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: r.canaryIngressName,
|
||||
Namespace: stableIngress.Namespace,
|
||||
Annotations: stableIngress.Annotations,
|
||||
Labels: stableIngress.Labels,
|
||||
},
|
||||
Spec: netv1.IngressSpec{
|
||||
Rules: make([]netv1.IngressRule, 0),
|
||||
IngressClassName: stableIngress.Spec.IngressClassName,
|
||||
TLS: stableIngress.Spec.TLS,
|
||||
},
|
||||
}
|
||||
hosts := sets.NewString()
|
||||
// Ensure canaryIngress is owned by this Rollout for cleanup
|
||||
desiredCanaryIngress.SetOwnerReferences([]metav1.OwnerReference{r.conf.OwnerRef})
|
||||
// Copy only the rules which reference the stableService from the stableIngress to the canaryIngress
|
||||
// and change service backend to canaryService. Rules **not** referencing the stableIngress will be ignored.
|
||||
for ir := 0; ir < len(stableIngress.Spec.Rules); ir++ {
|
||||
var hasStableServiceBackendRule bool
|
||||
stableRule := stableIngress.Spec.Rules[ir]
|
||||
canaryRule := netv1.IngressRule{
|
||||
Host: stableRule.Host,
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{},
|
||||
},
|
||||
}
|
||||
// Update all backends pointing to the stableService to point to the canaryService now
|
||||
for ip := 0; ip < len(stableRule.HTTP.Paths); ip++ {
|
||||
if stableRule.HTTP.Paths[ip].Backend.Service.Name == r.conf.StableService {
|
||||
hasStableServiceBackendRule = true
|
||||
if stableRule.Host != "" {
|
||||
hosts.Insert(stableRule.Host)
|
||||
}
|
||||
canaryPath := netv1.HTTPIngressPath{
|
||||
Path: stableRule.HTTP.Paths[ip].Path,
|
||||
PathType: stableRule.HTTP.Paths[ip].PathType,
|
||||
Backend: stableRule.HTTP.Paths[ip].Backend,
|
||||
}
|
||||
canaryPath.Backend.Service.Name = r.conf.CanaryService
|
||||
canaryRule.HTTP.Paths = append(canaryRule.HTTP.Paths, canaryPath)
|
||||
}
|
||||
}
|
||||
// If this rule was using the specified stableService backend, append it to the canary Ingress spec
|
||||
if hasStableServiceBackendRule {
|
||||
desiredCanaryIngress.Spec.Rules = append(desiredCanaryIngress.Spec.Rules, canaryRule)
|
||||
}
|
||||
}
|
||||
|
||||
return desiredCanaryIngress
|
||||
}
|
||||
|
||||
func defaultCanaryIngressName(name string) string {
|
||||
return fmt.Sprintf("%s-canary", name)
|
||||
}
|
||||
|
||||
func (r *ingressController) executeLuaForCanary(annotations map[string]string, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (map[string]string, error) {
|
||||
if weight == nil {
|
||||
// the lua script does not have a pointer type,
|
||||
// so we need to pass weight=-1 to indicate the case where weight is nil.
|
||||
weight = utilpointer.Int32(-1)
|
||||
}
|
||||
type LuaData struct {
|
||||
Annotations map[string]string
|
||||
Weight string
|
||||
Matches []rolloutv1alpha1.HttpRouteMatch
|
||||
CanaryService string
|
||||
}
|
||||
data := &LuaData{
|
||||
Annotations: annotations,
|
||||
Weight: fmt.Sprintf("%d", *weight),
|
||||
Matches: matches,
|
||||
CanaryService: r.conf.CanaryService,
|
||||
}
|
||||
unObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj := &unstructured.Unstructured{Object: unObj}
|
||||
l, err := r.luaManager.RunLuaScript(obj, r.luaScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
returnValue := l.Get(-1)
|
||||
if returnValue.Type() == lua.LTTable {
|
||||
jsonBytes, err := luamanager.Encode(returnValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newAnnotations := map[string]string{}
|
||||
err = json.Unmarshal(jsonBytes, &newAnnotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newAnnotations, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expect table output from Lua script, not %s", returnValue.Type().String())
|
||||
}
|
||||
|
||||
func (r *ingressController) getTrafficRoutingIngressLuaScript(iType string) (string, error) {
|
||||
if iType == "" {
|
||||
iType = "nginx"
|
||||
}
|
||||
luaScript, err := configuration.GetTrafficRoutingIngressLuaScript(r.Client, iType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if luaScript != "" {
|
||||
return luaScript, nil
|
||||
}
|
||||
key := fmt.Sprintf("lua_configuration/trafficrouting_ingress/%s.lua", iType)
|
||||
script := util.GetLuaConfigurationContent(key)
|
||||
if script == "" {
|
||||
return "", fmt.Errorf("%s lua script is not found", iType)
|
||||
}
|
||||
return script, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,604 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise Authors.
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
rolloutsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"github.com/openkruise/rollouts/pkg/util/configuration"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
)
|
||||
|
||||
var (
|
||||
demoConf = corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configuration.RolloutConfigurationName,
|
||||
Namespace: util.GetRolloutNamespace(),
|
||||
},
|
||||
Data: map[string]string{
|
||||
fmt.Sprintf("%s.nginx", configuration.LuaTrafficRoutingIngressTypePrefix): `
|
||||
annotations = obj.annotations
|
||||
annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = nil
|
||||
annotations["nginx.ingress.kubernetes.io/canary-weight"] = nil
|
||||
if ( obj.weight ~= "-1" )
|
||||
then
|
||||
annotations["nginx.ingress.kubernetes.io/canary-weight"] = obj.weight
|
||||
end
|
||||
if ( not obj.matches )
|
||||
then
|
||||
return annotations
|
||||
end
|
||||
for _,match in ipairs(obj.matches) do
|
||||
header = match.headers[1]
|
||||
if ( header.name == "canary-by-cookie" )
|
||||
then
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = header.value
|
||||
else
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header"] = header.name
|
||||
if ( header.type == "RegularExpression" )
|
||||
then
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = header.value
|
||||
else
|
||||
annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = header.value
|
||||
end
|
||||
end
|
||||
end
|
||||
return annotations
|
||||
`,
|
||||
fmt.Sprintf("%s.aliyun-alb", configuration.LuaTrafficRoutingIngressTypePrefix): `
|
||||
function split(input, delimiter)
|
||||
local arr = {}
|
||||
string.gsub(input, '[^' .. delimiter ..']+', function(w) table.insert(arr, w) end)
|
||||
return arr
|
||||
end
|
||||
annotations = obj.annotations
|
||||
annotations["alb.ingress.kubernetes.io/canary"] = "true"
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-cookie"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-header"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-header-pattern"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-by-header-value"] = nil
|
||||
annotations["alb.ingress.kubernetes.io/canary-weight"] = nil
|
||||
conditionKey = string.format("alb.ingress.kubernetes.io/conditions.%s", obj.canaryService)
|
||||
annotations[conditionKey] = nil
|
||||
if ( obj.weight ~= "-1" )
|
||||
then
|
||||
annotations["alb.ingress.kubernetes.io/canary-weight"] = obj.weight
|
||||
end
|
||||
if ( not obj.matches )
|
||||
then
|
||||
return annotations
|
||||
end
|
||||
if ( annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] )
|
||||
then
|
||||
protocolobj = json.decode(annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"])
|
||||
newprotocolobj = {}
|
||||
for _, v in pairs(protocolobj) do
|
||||
newprotocolobj[obj.canaryService] = v
|
||||
end
|
||||
annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] = json.encode(newprotocolobj)
|
||||
end
|
||||
|
||||
conditions = {}
|
||||
match = obj.matches[1]
|
||||
for _,header in ipairs(match.headers) do
|
||||
condition = {}
|
||||
if ( header.name == "Cookie" )
|
||||
then
|
||||
condition.type = "Cookie"
|
||||
condition.cookieConfig = {}
|
||||
cookies = split(header.value, ";")
|
||||
values = {}
|
||||
for _,cookieStr in ipairs(cookies) do
|
||||
cookie = split(cookieStr, "=")
|
||||
value = {}
|
||||
value.key = cookie[1]
|
||||
value.value = cookie[2]
|
||||
table.insert(values, value)
|
||||
end
|
||||
condition.cookieConfig.values = values
|
||||
elseif ( header.name == "SourceIp" )
|
||||
then
|
||||
condition.type = "SourceIp"
|
||||
condition.sourceIpConfig = {}
|
||||
ips = split(header.value, ";")
|
||||
values = {}
|
||||
for _,ip in ipairs(ips) do
|
||||
table.insert(values, ip)
|
||||
end
|
||||
condition.sourceIpConfig.values = values
|
||||
else
|
||||
condition.type = "Header"
|
||||
condition.headerConfig = {}
|
||||
condition.headerConfig.key = header.name
|
||||
vals = split(header.value, ";")
|
||||
values = {}
|
||||
for _,val in ipairs(vals) do
|
||||
table.insert(values, val)
|
||||
end
|
||||
condition.headerConfig.values = values
|
||||
end
|
||||
table.insert(conditions, condition)
|
||||
end
|
||||
annotations[conditionKey] = json.encode(conditions)
|
||||
return annotations
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
demoIngress = netv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "echoserver",
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "nginx",
|
||||
},
|
||||
},
|
||||
Spec: netv1.IngressSpec{
|
||||
IngressClassName: utilpointer.String("nginx"),
|
||||
TLS: []netv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"echoserver.example.com"},
|
||||
SecretName: "echoserver-name",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"log.example.com"},
|
||||
SecretName: "log-name",
|
||||
},
|
||||
},
|
||||
Rules: []netv1.IngressRule{
|
||||
{
|
||||
Host: "echoserver.example.com",
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{
|
||||
Paths: []netv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/apis/echo",
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: "echoserver",
|
||||
Port: netv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/apis/other",
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: "other",
|
||||
Port: netv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "log.example.com",
|
||||
IngressRuleValue: netv1.IngressRuleValue{
|
||||
HTTP: &netv1.HTTPIngressRuleValue{
|
||||
Paths: []netv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/apis/logs",
|
||||
Backend: netv1.IngressBackend{
|
||||
Service: &netv1.IngressServiceBackend{
|
||||
Name: "echoserver",
|
||||
Port: netv1.ServiceBackendPort{
|
||||
Number: 8899,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
scheme = runtime.NewScheme()
|
||||
_ = clientgoscheme.AddToScheme(scheme)
|
||||
_ = rolloutsv1alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getConfigmap func() *corev1.ConfigMap
|
||||
getIngress func() []*netv1.Ingress
|
||||
expectIngress func() *netv1.Ingress
|
||||
}{
|
||||
{
|
||||
name: "init test1",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy()}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
},
|
||||
}
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
fakeCli.Create(context.TODO(), cs.getConfigmap())
|
||||
for _, ingress := range cs.getIngress() {
|
||||
fakeCli.Create(context.TODO(), ingress)
|
||||
}
|
||||
controller, err := NewNginxIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewNginxIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
err = controller.Initialize(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryIngress)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary ingress failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
expect := cs.expectIngress()
|
||||
if !reflect.DeepEqual(canaryIngress.Annotations, expect.Annotations) ||
|
||||
!reflect.DeepEqual(canaryIngress.Spec, expect.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect), util.DumpJSON(canaryIngress))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRoutes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getConfigmap func() *corev1.ConfigMap
|
||||
getIngress func() []*netv1.Ingress
|
||||
getRoutes func() (*int32, []rolloutsv1alpha1.HttpRouteMatch)
|
||||
expectIngress func() *netv1.Ingress
|
||||
ingressType string
|
||||
}{
|
||||
{
|
||||
name: "ensure routes test1",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
return nil, []rolloutsv1alpha1.HttpRouteMatch{
|
||||
// header
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "user_id",
|
||||
Value: "123456",
|
||||
},
|
||||
},
|
||||
},
|
||||
// cookies
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "canary-by-cookie",
|
||||
Value: "demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure routes test2",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
return utilpointer.Int32(40), nil
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "40"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure routes test3",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
iType := gatewayv1alpha2.HeaderMatchRegularExpression
|
||||
return nil, []rolloutsv1alpha1.HttpRouteMatch{
|
||||
// header
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "user_id",
|
||||
Value: "123*",
|
||||
Type: &iType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
|
||||
expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = "123*"
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure routes test4",
|
||||
ingressType: "aliyun-alb",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["alb.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["alb.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
canary.Annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] = `{"echoserver":"http"}`
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
|
||||
return nil, []rolloutsv1alpha1.HttpRouteMatch{
|
||||
// header
|
||||
{
|
||||
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
|
||||
{
|
||||
Name: "Cookie",
|
||||
Value: "demo1=value1;demo2=value2",
|
||||
},
|
||||
{
|
||||
Name: "SourceIp",
|
||||
Value: "192.168.0.0/16;172.16.0.0/16",
|
||||
},
|
||||
{
|
||||
Name: "headername",
|
||||
Value: "headervalue1;headervalue2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
expect := demoIngress.DeepCopy()
|
||||
expect.Name = "echoserver-canary"
|
||||
expect.Annotations["alb.ingress.kubernetes.io/canary"] = "true"
|
||||
expect.Annotations["alb.ingress.kubernetes.io/backend-svcs-protocols"] = `{"echoserver-canary":"http"}`
|
||||
expect.Annotations["alb.ingress.kubernetes.io/conditions.echoserver-canary"] = `[{"cookieConfig":{"values":[{"key":"demo1","value":"value1"},{"key":"demo2","value":"value2"}]},"type":"Cookie"},{"sourceIpConfig":{"values":["192.168.0.0/16","172.16.0.0/16"]},"type":"SourceIp"},{"headerConfig":{"key":"headername","values":["headervalue1","headervalue2"]},"type":"Header"}]`
|
||||
expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1]
|
||||
expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
},
|
||||
}
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
fakeCli.Create(context.TODO(), cs.getConfigmap())
|
||||
for _, ingress := range cs.getIngress() {
|
||||
fakeCli.Create(context.TODO(), ingress)
|
||||
}
|
||||
config.TrafficConf.ClassType = cs.ingressType
|
||||
controller, err := NewNginxIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewNginxIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
weight, matches := cs.getRoutes()
|
||||
_, err = controller.EnsureRoutes(context.TODO(), weight, matches)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureRoutes failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryIngress)
|
||||
if err != nil {
|
||||
t.Fatalf("Get canary ingress failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
expect := cs.expectIngress()
|
||||
if !reflect.DeepEqual(canaryIngress.Annotations, expect.Annotations) ||
|
||||
!reflect.DeepEqual(canaryIngress.Spec, expect.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect), util.DumpJSON(canaryIngress))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalise(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
getConfigmap func() *corev1.ConfigMap
|
||||
getIngress func() []*netv1.Ingress
|
||||
expectIngress func() *netv1.Ingress
|
||||
}{
|
||||
{
|
||||
name: "finalise test1",
|
||||
getConfigmap: func() *corev1.ConfigMap {
|
||||
return demoConf.DeepCopy()
|
||||
},
|
||||
getIngress: func() []*netv1.Ingress {
|
||||
canary := demoIngress.DeepCopy()
|
||||
canary.Name = "echoserver-canary"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary"] = "true"
|
||||
canary.Annotations["nginx.ingress.kubernetes.io/canary-weight"] = "0"
|
||||
canary.Spec.Rules[0].HTTP.Paths = canary.Spec.Rules[0].HTTP.Paths[:1]
|
||||
canary.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
|
||||
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
|
||||
},
|
||||
expectIngress: func() *netv1.Ingress {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config := Config{
|
||||
RolloutName: "rollout-demo",
|
||||
StableService: "echoserver",
|
||||
CanaryService: "echoserver-canary",
|
||||
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
|
||||
Name: "echoserver",
|
||||
},
|
||||
}
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
fakeCli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
fakeCli.Create(context.TODO(), cs.getConfigmap())
|
||||
for _, ingress := range cs.getIngress() {
|
||||
fakeCli.Create(context.TODO(), ingress)
|
||||
}
|
||||
controller, err := NewNginxIngressTrafficRouting(fakeCli, config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewNginxIngressTrafficRouting failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
err = controller.Finalise(context.TODO())
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureRoutes failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
canaryIngress := &netv1.Ingress{}
|
||||
err = fakeCli.Get(context.TODO(), client.ObjectKey{Name: "echoserver-canary"}, canaryIngress)
|
||||
if err != nil {
|
||||
if cs.expectIngress() == nil && errors.IsNotFound(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Get canary ingress failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
expect := cs.expectIngress()
|
||||
if !reflect.DeepEqual(canaryIngress.Annotations, expect.Annotations) ||
|
||||
!reflect.DeepEqual(canaryIngress.Spec, expect.Spec) {
|
||||
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect), util.DumpJSON(canaryIngress))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue