Signed-off-by: liheng.zms <liheng.zms@alibaba-inc.com> (#153)

Add TrafficRouting CRD for end-to-end canary deployment
This commit is contained in:
berg 2023-06-26 15:55:58 +08:00 committed by GitHub
parent 3578b399a6
commit 7139171497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2347 additions and 668 deletions

View File

@ -81,13 +81,13 @@ jobs:
done
set +e
PODS=$(kubectl get pod -n kruise-rollout | grep '1/1' | wc -l)
kubectl get node -o yaml
kubectl get all -n kruise-rollout -o yaml
set -e
if [ "$PODS" -eq "1" ]; then
echo "Wait for kruise-rollout ready successfully"
else
echo "Timeout to wait for kruise-rollout ready"
kubectl get pod -n kruise-rollout --no-headers | awk '{print $1}' | xargs kubectl logs -p -n kruise-rollout
kubectl get pod -n kruise-rollout --no-headers | awk '{print $1}' | xargs kubectl logs -n kruise-rollout
exit 1
fi
- name: Run E2E Tests For Deployment Controller

View File

@ -51,6 +51,12 @@ const (
// Defaults to "canary" to Deployment.
// Defaults to "partition" to the others.
RolloutStyleAnnotation = "rollouts.kruise.io/rolling-style"
// TrafficRoutingAnnotation is the TrafficRouting Name, and it is the Rollout's TrafficRouting.
// The Rollout release will trigger the TrafficRouting release. For example:
// A microservice consists of three applications, and the invocation relationship is as follows: a -> b -> c,
// and application(a, b, c)'s gateway is trafficRouting. Any application(a, b or b) release will trigger TrafficRouting release.
TrafficRoutingAnnotation = "rollouts.kruise.io/trafficrouting"
)
// RolloutSpec defines the desired state of Rollout
@ -72,19 +78,8 @@ type ObjectRef struct {
// WorkloadRef contains enough information to let you identify a workload for Rollout
// Batch release of the bypass
WorkloadRef *WorkloadRef `json:"workloadRef,omitempty"`
// revisionRef
// Fully managed batch publishing capability
//RevisionRef *ControllerRevisionRef `json:"revisionRef,omitempty"`
}
type ObjectRefType string
const (
WorkloadRefType = "workloadRef"
RevisionRefType = "revisionRef"
)
// WorkloadRef holds a references to the Kubernetes object
type WorkloadRef struct {
// API Version of the referent
@ -102,50 +97,33 @@ type RolloutStrategy struct {
Paused bool `json:"paused,omitempty"`
// +optional
Canary *CanaryStrategy `json:"canary,omitempty"`
// +optional
// BlueGreen *BlueGreenStrategy `json:"blueGreen,omitempty"`
}
type RolloutStrategyType string
const (
RolloutStrategyCanary RolloutStrategyType = "canary"
RolloutStrategyBlueGreen RolloutStrategyType = "blueGreen"
)
// CanaryStrategy defines parameters for a Replica Based Canary
type CanaryStrategy struct {
// Steps define the order of phases to execute release in batches(20%, 40%, 60%, 80%, 100%)
// +optional
Steps []CanaryStep `json:"steps,omitempty"`
// TrafficRoutings hosts all the supported service meshes supported to enable more fine-grained traffic routing
// todo current only support one TrafficRouting
TrafficRoutings []*TrafficRouting `json:"trafficRoutings,omitempty"`
// and current only support one TrafficRouting
TrafficRoutings []TrafficRoutingRef `json:"trafficRoutings,omitempty"`
// FailureThreshold indicates how many failed pods can be tolerated in all upgraded pods.
// Only when FailureThreshold are satisfied, Rollout can enter ready state.
// If FailureThreshold is nil, Rollout will use the MaxUnavailable of workload as its
// FailureThreshold.
// Defaults to nil.
FailureThreshold *intstr.IntOrString `json:"failureThreshold,omitempty"`
// MetricsAnalysis *MetricsAnalysisBackground `json:"metricsAnalysis,omitempty"`
}
// CanaryStep defines a step of a canary workload.
type CanaryStep struct {
// Weight indicate how many percentage of traffic the canary pods should receive
// +optional
Weight *int32 `json:"weight,omitempty"`
TrafficRoutingStrategy `json:",inline"`
// Replicas is the number of expected canary pods in this batch
// it can be an absolute number (ex: 5) or a percentage of total pods.
Replicas *intstr.IntOrString `json:"replicas,omitempty"`
// Pause defines a pause stage for a rollout, manual or auto
// +optional
Pause RolloutPause `json:"pause,omitempty"`
// Matches define conditions used for matching the incoming HTTP requests to canary service.
// Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied.
// If Gateway API, current only support one match.
// And cannot support both weight and matches, if both are configured, then matches takes precedence.
Matches []HttpRouteMatch `json:"matches,omitempty"`
}
type HttpRouteMatch struct {
@ -163,37 +141,6 @@ type RolloutPause struct {
Duration *int32 `json:"duration,omitempty"`
}
// TrafficRouting hosts all the different configuration for supported service meshes to enable more fine-grained traffic routing
type TrafficRouting struct {
// Service holds the name of a service which selects pods with stable version and don't select any pods with canary version.
Service string `json:"service"`
// Optional duration in seconds the traffic provider(e.g. nginx ingress controller) consumes the service, ingress configuration changes gracefully.
GracePeriodSeconds int32 `json:"gracePeriodSeconds,omitempty"`
// Ingress holds Ingress specific configuration to route traffic, e.g. Nginx, Alb.
Ingress *IngressTrafficRouting `json:"ingress,omitempty"`
// Gateway holds Gateway specific configuration to route traffic
// Gateway configuration only supports >= v0.4.0 (v1alpha2).
Gateway *GatewayTrafficRouting `json:"gateway,omitempty"`
}
// IngressTrafficRouting configuration for ingress controller to control traffic routing
type IngressTrafficRouting struct {
// ClassType refers to the type of `Ingress`.
// current support nginx, aliyun-alb. default is nginx.
// +optional
ClassType string `json:"classType,omitempty"`
// Name refers to the name of an `Ingress` resource in the same namespace as the `Rollout`
Name string `json:"name"`
}
// GatewayTrafficRouting configuration for gateway api
type GatewayTrafficRouting struct {
// HTTPRouteName refers to the name of an `HTTPRoute` resource in the same namespace as the `Rollout`
HTTPRouteName *string `json:"httpRouteName,omitempty"`
// TCPRouteName *string `json:"tcpRouteName,omitempty"`
// UDPRouteName *string `json:"udpRouteName,omitempty"`
}
// RolloutStatus defines the observed state of Rollout
type RolloutStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster

View File

@ -0,0 +1,153 @@
/*
Copyright 2023 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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
ProgressingRolloutFinalizerPrefix = "progressing.rollouts.kruise.io"
)
// TrafficRoutingRef hosts all the different configuration for supported service meshes to enable more fine-grained traffic routing
type TrafficRoutingRef struct {
// Service holds the name of a service which selects pods with stable version and don't select any pods with canary version.
Service string `json:"service"`
// Optional duration in seconds the traffic provider(e.g. nginx ingress controller) consumes the service, ingress configuration changes gracefully.
GracePeriodSeconds int32 `json:"gracePeriodSeconds,omitempty"`
// Ingress holds Ingress specific configuration to route traffic, e.g. Nginx, Alb.
Ingress *IngressTrafficRouting `json:"ingress,omitempty"`
// Gateway holds Gateway specific configuration to route traffic
// Gateway configuration only supports >= v0.4.0 (v1alpha2).
Gateway *GatewayTrafficRouting `json:"gateway,omitempty"`
}
// IngressTrafficRouting configuration for ingress controller to control traffic routing
type IngressTrafficRouting struct {
// ClassType refers to the type of `Ingress`.
// current support nginx, aliyun-alb. default is nginx.
// +optional
ClassType string `json:"classType,omitempty"`
// Name refers to the name of an `Ingress` resource in the same namespace as the `Rollout`
Name string `json:"name"`
}
// GatewayTrafficRouting configuration for gateway api
type GatewayTrafficRouting struct {
// HTTPRouteName refers to the name of an `HTTPRoute` resource in the same namespace as the `Rollout`
HTTPRouteName *string `json:"httpRouteName,omitempty"`
// TCPRouteName *string `json:"tcpRouteName,omitempty"`
// UDPRouteName *string `json:"udpRouteName,omitempty"`
}
type TrafficRoutingSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// ObjectRef indicates trafficRouting ref
ObjectRef []TrafficRoutingRef `json:"objectRef"`
// trafficrouting strategy
Strategy TrafficRoutingStrategy `json:"strategy"`
}
type TrafficRoutingStrategy struct {
// Weight indicate how many percentage of traffic the canary pods should receive
// +optional
Weight *int32 `json:"weight,omitempty"`
// Set overwrites the request with the given header (name, value)
// before the action.
//
// Input:
// GET /foo HTTP/1.1
// my-header: foo
//
// requestHeaderModifier:
// set:
// - name: "my-header"
// value: "bar"
//
// Output:
// GET /foo HTTP/1.1
// my-header: bar
//
// +optional
// RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter `json:"requestHeaderModifier,omitempty"`
// Matches define conditions used for matching the incoming HTTP requests to canary service.
// Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied.
// If Gateway API, current only support one match.
// And cannot support both weight and matches, if both are configured, then matches takes precedence.
Matches []HttpRouteMatch `json:"matches,omitempty"`
}
type TrafficRoutingStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// observedGeneration is the most recent generation observed for this Rollout.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Phase is the trafficRouting phase.
Phase TrafficRoutingPhase `json:"phase,omitempty"`
// Message provides details on why the rollout is in its current phase
Message string `json:"message,omitempty"`
}
// TrafficRoutingPhase are a set of phases that this rollout
type TrafficRoutingPhase string
const (
// TrafficRoutingPhaseInitial indicates a traffic routing is Initial
TrafficRoutingPhaseInitial TrafficRoutingPhase = "Initial"
// TrafficRoutingPhaseHealthy indicates a traffic routing is healthy.
// This means that Ingress and Service Resources exist.
TrafficRoutingPhaseHealthy TrafficRoutingPhase = "Healthy"
// TrafficRoutingPhaseProgressing indicates a traffic routing is not yet healthy but still making progress towards a healthy state
TrafficRoutingPhaseProgressing TrafficRoutingPhase = "Progressing"
// TrafficRoutingPhaseFinalizing indicates the trafficRouting progress is complete, and is running recycle operations.
TrafficRoutingPhaseFinalizing TrafficRoutingPhase = "Finalizing"
// TrafficRoutingPhaseTerminating indicates a traffic routing is terminated
TrafficRoutingPhaseTerminating TrafficRoutingPhase = "Terminating"
)
// +genclient
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.phase",description="The TrafficRouting status phase"
// +kubebuilder:printcolumn:name="MESSAGE",type="string",JSONPath=".status.message",description="The TrafficRouting canary status message"
// +kubebuilder:printcolumn:name="AGE",type=date,JSONPath=".metadata.creationTimestamp"
// TrafficRouting is the Schema for the TrafficRoutings API
type TrafficRouting struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TrafficRoutingSpec `json:"spec,omitempty"`
Status TrafficRoutingStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// TrafficRoutingList contains a list of TrafficRouting
type TrafficRoutingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TrafficRouting `json:"items"`
}
func init() {
SchemeBuilder.Register(&TrafficRouting{}, &TrafficRoutingList{})
}

View File

@ -178,24 +178,13 @@ func (in *CanaryStatus) DeepCopy() *CanaryStatus {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CanaryStep) DeepCopyInto(out *CanaryStep) {
*out = *in
if in.Weight != nil {
in, out := &in.Weight, &out.Weight
*out = new(int32)
**out = **in
}
in.TrafficRoutingStrategy.DeepCopyInto(&out.TrafficRoutingStrategy)
if in.Replicas != nil {
in, out := &in.Replicas, &out.Replicas
*out = new(intstr.IntOrString)
**out = **in
}
in.Pause.DeepCopyInto(&out.Pause)
if in.Matches != nil {
in, out := &in.Matches, &out.Matches
*out = make([]HttpRouteMatch, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryStep.
@ -240,13 +229,9 @@ func (in *CanaryStrategy) DeepCopyInto(out *CanaryStrategy) {
}
if in.TrafficRoutings != nil {
in, out := &in.TrafficRoutings, &out.TrafficRoutings
*out = make([]*TrafficRouting, len(*in))
*out = make([]TrafficRoutingRef, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(TrafficRouting)
(*in).DeepCopyInto(*out)
}
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailureThreshold != nil {
@ -783,16 +768,10 @@ func (in *ServiceInfo) DeepCopy() *ServiceInfo {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TrafficRouting) DeepCopyInto(out *TrafficRouting) {
*out = *in
if in.Ingress != nil {
in, out := &in.Ingress, &out.Ingress
*out = new(IngressTrafficRouting)
**out = **in
}
if in.Gateway != nil {
in, out := &in.Gateway, &out.Gateway
*out = new(GatewayTrafficRouting)
(*in).DeepCopyInto(*out)
}
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRouting.
@ -805,6 +784,14 @@ func (in *TrafficRouting) DeepCopy() *TrafficRouting {
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TrafficRouting) 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 *TrafficRoutingInfo) DeepCopyInto(out *TrafficRoutingInfo) {
*out = *in
@ -830,6 +817,128 @@ func (in *TrafficRoutingInfo) DeepCopy() *TrafficRoutingInfo {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TrafficRoutingList) DeepCopyInto(out *TrafficRoutingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TrafficRouting, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRoutingList.
func (in *TrafficRoutingList) DeepCopy() *TrafficRoutingList {
if in == nil {
return nil
}
out := new(TrafficRoutingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TrafficRoutingList) 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 *TrafficRoutingRef) DeepCopyInto(out *TrafficRoutingRef) {
*out = *in
if in.Ingress != nil {
in, out := &in.Ingress, &out.Ingress
*out = new(IngressTrafficRouting)
**out = **in
}
if in.Gateway != nil {
in, out := &in.Gateway, &out.Gateway
*out = new(GatewayTrafficRouting)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRoutingRef.
func (in *TrafficRoutingRef) DeepCopy() *TrafficRoutingRef {
if in == nil {
return nil
}
out := new(TrafficRoutingRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TrafficRoutingSpec) DeepCopyInto(out *TrafficRoutingSpec) {
*out = *in
if in.ObjectRef != nil {
in, out := &in.ObjectRef, &out.ObjectRef
*out = make([]TrafficRoutingRef, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Strategy.DeepCopyInto(&out.Strategy)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRoutingSpec.
func (in *TrafficRoutingSpec) DeepCopy() *TrafficRoutingSpec {
if in == nil {
return nil
}
out := new(TrafficRoutingSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TrafficRoutingStatus) DeepCopyInto(out *TrafficRoutingStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRoutingStatus.
func (in *TrafficRoutingStatus) DeepCopy() *TrafficRoutingStatus {
if in == nil {
return nil
}
out := new(TrafficRoutingStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TrafficRoutingStrategy) DeepCopyInto(out *TrafficRoutingStrategy) {
*out = *in
if in.Weight != nil {
in, out := &in.Weight, &out.Weight
*out = new(int32)
**out = **in
}
if in.Matches != nil {
in, out := &in.Matches, &out.Matches
*out = make([]HttpRouteMatch, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRoutingStrategy.
func (in *TrafficRoutingStrategy) DeepCopy() *TrafficRoutingStrategy {
if in == nil {
return nil
}
out := new(TrafficRoutingStrategy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WorkloadInfo) DeepCopyInto(out *WorkloadInfo) {
*out = *in

View File

@ -111,13 +111,20 @@ spec:
description: CanaryStep defines a step of a canary workload.
properties:
matches:
description: Matches define conditions used for matching
the incoming HTTP requests to canary service. Each
match is independent, i.e. this rule will be matched
if **any** one of the matches is satisfied. If Gateway
API, current only support one match. And cannot support
both weight and matches, if both are configured, then
matches takes precedence.
description: "Set overwrites the request with the given
header (name, value) before the action. \n Input:
\ GET /foo HTTP/1.1 my-header: foo \n requestHeaderModifier:
\ set: - name: \"my-header\" value: \"bar\"
\n Output: GET /foo HTTP/1.1 my-header: bar \n
RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter
`json:\"requestHeaderModifier,omitempty\"` Matches
define conditions used for matching the incoming HTTP
requests to canary service. Each match is independent,
i.e. this rule will be matched if **any** one of the
matches is satisfied. If Gateway API, current only
support one match. And cannot support both weight
and matches, if both are configured, then matches
takes precedence."
items:
properties:
headers:
@ -209,9 +216,9 @@ spec:
trafficRoutings:
description: TrafficRoutings hosts all the supported service
meshes supported to enable more fine-grained traffic routing
todo current only support one TrafficRouting
and current only support one TrafficRouting
items:
description: TrafficRouting hosts all the different configuration
description: TrafficRoutingRef hosts all the different configuration
for supported service meshes to enable more fine-grained
traffic routing
properties:

View File

@ -0,0 +1,205 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
creationTimestamp: null
name: trafficroutings.rollouts.kruise.io
spec:
group: rollouts.kruise.io
names:
kind: TrafficRouting
listKind: TrafficRoutingList
plural: trafficroutings
singular: trafficrouting
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: The TrafficRouting status phase
jsonPath: .status.phase
name: STATUS
type: string
- description: The TrafficRouting canary status message
jsonPath: .status.message
name: MESSAGE
type: string
- jsonPath: .metadata.creationTimestamp
name: AGE
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: TrafficRouting is the Schema for the TrafficRoutings API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
properties:
objectRef:
description: ObjectRef indicates trafficRouting ref
items:
description: TrafficRoutingRef hosts all the different configuration
for supported service meshes to enable more fine-grained traffic
routing
properties:
gateway:
description: Gateway holds Gateway specific configuration to
route traffic Gateway configuration only supports >= v0.4.0
(v1alpha2).
properties:
httpRouteName:
description: HTTPRouteName refers to the name of an `HTTPRoute`
resource in the same namespace as the `Rollout`
type: string
type: object
gracePeriodSeconds:
description: Optional duration in seconds the traffic provider(e.g.
nginx ingress controller) consumes the service, ingress configuration
changes gracefully.
format: int32
type: integer
ingress:
description: Ingress holds Ingress specific configuration to
route traffic, e.g. Nginx, Alb.
properties:
classType:
description: ClassType refers to the type of `Ingress`.
current support nginx, aliyun-alb. default is nginx.
type: string
name:
description: Name refers to the name of an `Ingress` resource
in the same namespace as the `Rollout`
type: string
required:
- name
type: object
service:
description: Service holds the name of a service which selects
pods with stable version and don't select any pods with canary
version.
type: string
required:
- service
type: object
type: array
strategy:
description: trafficrouting strategy
properties:
matches:
description: "Set overwrites the request with the given header
(name, value) before the action. \n Input: GET /foo HTTP/1.1
\ my-header: foo \n requestHeaderModifier: set: - name:
\"my-header\" value: \"bar\" \n Output: GET /foo HTTP/1.1
\ my-header: bar \n RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter
`json:\"requestHeaderModifier,omitempty\"` Matches define conditions
used for matching the incoming HTTP requests to canary service.
Each match is independent, i.e. this rule will be matched if
**any** one of the matches is satisfied. If Gateway API, current
only support one match. And cannot support both weight and matches,
if both are configured, then matches takes precedence."
items:
properties:
headers:
description: Headers specifies HTTP request header matchers.
Multiple match values are ANDed together, meaning, a request
must match all the specified headers to select the route.
items:
description: HTTPHeaderMatch describes how to select a
HTTP route by matching HTTP request headers.
properties:
name:
description: "Name is the name of the HTTP Header
to be matched. Name matching MUST be case insensitive.
(See https://tools.ietf.org/html/rfc7230#section-3.2).
\n If multiple entries specify equivalent header
names, only the first entry with an equivalent name
MUST be considered for a match. Subsequent entries
with an equivalent header name MUST be ignored.
Due to the case-insensitivity of header names, \"foo\"
and \"Foo\" are considered equivalent. \n When a
header is repeated in an HTTP request, it is implementation-specific
behavior as to how this is represented. Generally,
proxies should follow the guidance from the RFC:
https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2
regarding processing a repeated header, with special
handling for \"Set-Cookie\"."
maxLength: 256
minLength: 1
pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
type: string
type:
default: Exact
description: "Type specifies how to match against
the value of the header. \n Support: Core (Exact)
\n Support: Custom (RegularExpression) \n Since
RegularExpression HeaderMatchType has custom conformance,
implementations can support POSIX, PCRE or any other
dialects of regular expressions. Please read the
implementation's documentation to determine the
supported dialect."
enum:
- Exact
- RegularExpression
type: string
value:
description: Value is the value of HTTP Header to
be matched.
maxLength: 4096
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 16
type: array
type: object
type: array
weight:
description: Weight indicate how many percentage of traffic the
canary pods should receive
format: int32
type: integer
type: object
required:
- objectRef
- strategy
type: object
status:
properties:
message:
description: Message provides details on why the rollout is in its
current phase
type: string
observedGeneration:
description: observedGeneration is the most recent generation observed
for this Rollout.
format: int64
type: integer
phase:
description: Phase is the trafficRouting phase.
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -5,6 +5,7 @@ resources:
- bases/rollouts.kruise.io_rollouts.yaml
- bases/rollouts.kruise.io_batchreleases.yaml
- bases/rollouts.kruise.io_rollouthistories.yaml
- bases/rollouts.kruise.io_trafficroutings.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:

View File

@ -340,3 +340,28 @@ rules:
- get
- patch
- update
- apiGroups:
- rollouts.kruise.io
resources:
- trafficroutings
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- rollouts.kruise.io
resources:
- trafficroutings/finalizers
verbs:
- update
- apiGroups:
- rollouts.kruise.io
resources:
- trafficroutings/status
verbs:
- get
- patch
- update

View File

@ -0,0 +1,45 @@
function split(input, delimiter)
local arr = {}
string.gsub(input, '[^' .. delimiter ..']+', function(w) table.insert(arr, w) end)
return arr
end
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 ( obj.requestHeaderModifier )
then
local str = ''
for _,header in ipairs(obj.requestHeaderModifier.set) do
str = str..string.format("%s %s", header.name, header.value)
end
annotations["mse.ingress.kubernetes.io/request-header-control-update"] = str
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

10
main.go
View File

@ -27,6 +27,7 @@ import (
"github.com/openkruise/rollouts/pkg/controller/deployment"
"github.com/openkruise/rollouts/pkg/controller/rollout"
"github.com/openkruise/rollouts/pkg/controller/rollouthistory"
"github.com/openkruise/rollouts/pkg/controller/trafficrouting"
utilclient "github.com/openkruise/rollouts/pkg/util/client"
utilfeature "github.com/openkruise/rollouts/pkg/util/feature"
"github.com/openkruise/rollouts/pkg/webhook"
@ -110,6 +111,15 @@ func main() {
os.Exit(1)
}
if err = (&trafficrouting.TrafficRoutingReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("trafficrouting-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "TrafficRouting")
os.Exit(1)
}
if err = br.Add(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "BatchRelease")
os.Exit(1)

View File

@ -132,6 +132,7 @@ func (r *realCanaryController) create(release *v1alpha1.BatchRelease, template *
// spec
canary.Spec = *template.Spec.DeepCopy()
// todo, patch canary pod metadata
canary.Spec.Replicas = pointer.Int32Ptr(0)
canary.Spec.Paused = false
@ -179,6 +180,7 @@ func filterCanaryDeployment(ds []*apps.Deployment, template *corev1.PodTemplateS
return ds[0]
}
for _, d := range ds {
// todo, remove the canary pod metadata
if util.EqualIgnoreHash(template, &d.Spec.Template) {
return d
}

View File

@ -73,7 +73,6 @@ func (rc *realController) BuildCanaryController(release *v1alpha1.BatchRelease)
if client.IgnoreNotFound(err) != nil {
return rc, err
}
rc.canaryObject = filterCanaryDeployment(util.FilterActiveDeployment(ds), template)
if rc.canaryObject == nil {
return rc, control.GenerateNotFoundError(fmt.Sprintf("%v-canary", rc.stableKey), "Deployment")

View File

@ -45,7 +45,7 @@ type canaryReleaseManager struct {
recorder record.EventRecorder
}
func (m *canaryReleaseManager) runCanary(c *util.RolloutContext) error {
func (m *canaryReleaseManager) runCanary(c *RolloutContext) error {
canaryStatus := c.NewStatus.CanaryStatus
if br, err := m.fetchBatchRelease(c.Rollout.Namespace, c.Rollout.Name); err != nil && !errors.IsNotFound(err) {
klog.Errorf("rollout(%s/%s) fetch batchRelease failed: %s", c.Rollout.Namespace, c.Rollout.Name, err.Error())
@ -70,7 +70,9 @@ func (m *canaryReleaseManager) runCanary(c *util.RolloutContext) error {
// We need to clean up the canary-related resources first and then rollout the rest of the batch.
currentStep := c.Rollout.Spec.Strategy.Canary.Steps[canaryStatus.CurrentStepIndex-1]
if currentStep.Weight == nil && len(currentStep.Matches) == 0 {
done, err := m.trafficRoutingManager.FinalisingTrafficRouting(c, false)
tr := newTrafficRoutingContext(c)
done, err := m.trafficRoutingManager.FinalisingTrafficRouting(tr, false)
c.NewStatus.CanaryStatus.LastUpdateTime = tr.LastUpdateTime
if err != nil {
return err
} else if !done {
@ -95,7 +97,9 @@ func (m *canaryReleaseManager) runCanary(c *util.RolloutContext) error {
case v1alpha1.CanaryStepStateTrafficRouting:
klog.Infof("rollout(%s/%s) run canary strategy, and state(%s)", c.Rollout.Namespace, c.Rollout.Name, v1alpha1.CanaryStepStateTrafficRouting)
done, err := m.trafficRoutingManager.DoTrafficRouting(c)
tr := newTrafficRoutingContext(c)
done, err := m.trafficRoutingManager.DoTrafficRouting(tr)
c.NewStatus.CanaryStatus.LastUpdateTime = tr.LastUpdateTime
if err != nil {
return err
} else if done {
@ -142,6 +146,7 @@ func (m *canaryReleaseManager) runCanary(c *util.RolloutContext) error {
klog.Infof("rollout(%s/%s) canary run all steps, and completed", c.Rollout.Namespace, c.Rollout.Name)
canaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now()}
canaryStatus.CurrentStepState = v1alpha1.CanaryStepStateCompleted
return nil
}
klog.Infof("rollout(%s/%s) step(%d) state from(%s) -> to(%s)", c.Rollout.Namespace, c.Rollout.Name,
canaryStatus.CurrentStepIndex, v1alpha1.CanaryStepStateReady, canaryStatus.CurrentStepState)
@ -153,7 +158,7 @@ func (m *canaryReleaseManager) runCanary(c *util.RolloutContext) error {
return nil
}
func (m *canaryReleaseManager) doCanaryUpgrade(c *util.RolloutContext) (bool, error) {
func (m *canaryReleaseManager) doCanaryUpgrade(c *RolloutContext) (bool, error) {
// verify whether batchRelease configuration is the latest
steps := len(c.Rollout.Spec.Strategy.Canary.Steps)
canaryStatus := c.NewStatus.CanaryStatus
@ -184,12 +189,12 @@ func (m *canaryReleaseManager) doCanaryUpgrade(c *util.RolloutContext) (bool, er
return true, nil
}
func (m *canaryReleaseManager) doCanaryMetricsAnalysis(c *util.RolloutContext) (bool, error) {
func (m *canaryReleaseManager) doCanaryMetricsAnalysis(c *RolloutContext) (bool, error) {
// todo
return true, nil
}
func (m *canaryReleaseManager) doCanaryPaused(c *util.RolloutContext) (bool, error) {
func (m *canaryReleaseManager) doCanaryPaused(c *RolloutContext) (bool, error) {
canaryStatus := c.NewStatus.CanaryStatus
currentStep := c.Rollout.Spec.Strategy.Canary.Steps[canaryStatus.CurrentStepIndex-1]
steps := len(c.Rollout.Spec.Strategy.Canary.Steps)
@ -223,7 +228,7 @@ func (m *canaryReleaseManager) doCanaryPaused(c *util.RolloutContext) (bool, err
}
// cleanup after rollout is completed or finished
func (m *canaryReleaseManager) doCanaryFinalising(c *util.RolloutContext) (bool, error) {
func (m *canaryReleaseManager) doCanaryFinalising(c *RolloutContext) (bool, error) {
// when CanaryStatus is nil, which means canary action hasn't started yet, don't need doing cleanup
if c.NewStatus.CanaryStatus == nil {
return true, nil
@ -233,8 +238,10 @@ func (m *canaryReleaseManager) doCanaryFinalising(c *util.RolloutContext) (bool,
if err != nil {
return false, err
}
tr := newTrafficRoutingContext(c)
// 2. remove stable service the pod revision selector, so stable service will be selector all version pods.
done, err := m.trafficRoutingManager.FinalisingTrafficRouting(c, true)
done, err := m.trafficRoutingManager.FinalisingTrafficRouting(tr, true)
c.NewStatus.CanaryStatus.LastUpdateTime = tr.LastUpdateTime
if err != nil || !done {
return done, err
}
@ -244,7 +251,8 @@ func (m *canaryReleaseManager) doCanaryFinalising(c *util.RolloutContext) (bool,
return done, err
}
// 4. modify network api(ingress or gateway api) configuration, and route 100% traffic to stable pods.
done, err = m.trafficRoutingManager.FinalisingTrafficRouting(c, false)
done, err = m.trafficRoutingManager.FinalisingTrafficRouting(tr, false)
c.NewStatus.CanaryStatus.LastUpdateTime = tr.LastUpdateTime
if err != nil || !done {
return done, err
}
@ -260,7 +268,7 @@ func (m *canaryReleaseManager) doCanaryFinalising(c *util.RolloutContext) (bool,
return true, nil
}
func (m *canaryReleaseManager) removeRolloutProgressingAnnotation(c *util.RolloutContext) error {
func (m *canaryReleaseManager) removeRolloutProgressingAnnotation(c *RolloutContext) error {
if c.Workload == nil {
return nil
}
@ -270,10 +278,8 @@ func (m *canaryReleaseManager) removeRolloutProgressingAnnotation(c *util.Rollou
workloadRef := c.Rollout.Spec.ObjectRef.WorkloadRef
workloadGVK := schema.FromAPIVersionAndKind(workloadRef.APIVersion, workloadRef.Kind)
obj := util.GetEmptyWorkloadObject(workloadGVK)
if err := m.Get(context.TODO(), types.NamespacedName{Name: c.Workload.Name, Namespace: c.Workload.Namespace}, obj); err != nil {
klog.Errorf("getting updated workload(%s.%s) failed: %s", c.Workload.Namespace, c.Workload.Name, err.Error())
return err
}
obj.SetNamespace(c.Workload.Namespace)
obj.SetName(c.Workload.Name)
body := fmt.Sprintf(`{"metadata":{"annotations":{"%s":null}}}`, util.InRolloutProgressingAnnotation)
if err := m.Patch(context.TODO(), obj, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
klog.Errorf("rollout(%s/%s) patch workload(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, c.Workload.Name, err.Error())
@ -359,6 +365,7 @@ func createBatchRelease(rollout *v1alpha1.Rollout, rolloutID string, batch int32
RolloutID: rolloutID,
BatchPartition: utilpointer.Int32Ptr(batch),
FailureThreshold: rollout.Spec.Strategy.Canary.FailureThreshold,
// PatchPodTemplateMetadata: rollout.Spec.Strategy.Canary.PatchPodTemplateMetadata,
},
},
}
@ -375,7 +382,7 @@ func createBatchRelease(rollout *v1alpha1.Rollout, rolloutID string, batch int32
return br
}
func (m *canaryReleaseManager) removeBatchRelease(c *util.RolloutContext) (bool, error) {
func (m *canaryReleaseManager) removeBatchRelease(c *RolloutContext) (bool, error) {
batch := &v1alpha1.BatchRelease{}
err := m.Get(context.TODO(), client.ObjectKey{Namespace: c.Rollout.Namespace, Name: c.Rollout.Name}, batch)
if err != nil && errors.IsNotFound(err) {
@ -399,7 +406,7 @@ func (m *canaryReleaseManager) removeBatchRelease(c *util.RolloutContext) (bool,
return false, nil
}
func (m *canaryReleaseManager) finalizingBatchRelease(c *util.RolloutContext) (bool, error) {
func (m *canaryReleaseManager) finalizingBatchRelease(c *RolloutContext) (bool, error) {
br, err := m.fetchBatchRelease(c.Rollout.Namespace, c.Rollout.Name)
if err != nil {
if errors.IsNotFound(err) {

View File

@ -233,13 +233,16 @@ func TestRunCanary(t *testing.T) {
trafficRoutingManager: r.trafficRoutingManager,
recorder: r.Recorder,
}
workload, _ := r.finder.GetWorkloadForRef(rollout)
c := &util.RolloutContext{
workload, err := r.finder.GetWorkloadForRef(rollout)
if err != nil {
t.Fatalf("GetWorkloadForRef failed: %s", err.Error())
}
c := &RolloutContext{
Rollout: rollout,
NewStatus: rollout.Status.DeepCopy(),
Workload: workload,
}
err := r.canaryManager.runCanary(c)
err = r.canaryManager.runCanary(c)
if err != nil {
t.Fatalf("reconcileRolloutProgressing failed: %s", err.Error())
}
@ -310,7 +313,7 @@ func TestRunCanaryPaused(t *testing.T) {
trafficRoutingManager: r.trafficRoutingManager,
recorder: r.Recorder,
}
c := &util.RolloutContext{
c := &RolloutContext{
Rollout: rollout,
NewStatus: rollout.Status.DeepCopy(),
}

View File

@ -32,6 +32,7 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
utilpointer "k8s.io/utils/pointer"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
var (
@ -57,23 +58,31 @@ var (
Canary: &v1alpha1.CanaryStrategy{
Steps: []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(5),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(5),
},
Replicas: &intstr.IntOrString{IntVal: 1},
},
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Replicas: &intstr.IntOrString{IntVal: 2},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Replicas: &intstr.IntOrString{IntVal: 6},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Replicas: &intstr.IntOrString{IntVal: 10},
},
},
TrafficRoutings: []*v1alpha1.TrafficRouting{
TrafficRoutings: []v1alpha1.TrafficRoutingRef{
{
Service: "echoserver",
Ingress: &v1alpha1.IngressTrafficRouting{
@ -308,6 +317,36 @@ var (
`,
},
}
demoTR = &v1alpha1.TrafficRouting{
ObjectMeta: metav1.ObjectMeta{
Name: "tr-demo",
Labels: map[string]string{},
},
Spec: v1alpha1.TrafficRoutingSpec{
ObjectRef: []v1alpha1.TrafficRoutingRef{
{
Service: "echoserver",
Ingress: &v1alpha1.IngressTrafficRouting{
Name: "echoserver",
},
},
},
Strategy: v1alpha1.TrafficRoutingStrategy{
Matches: []v1alpha1.HttpRouteMatch{
// header
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "user_id",
Value: "123456",
},
},
},
},
},
},
}
)
func init() {

View File

@ -17,20 +17,36 @@ limitations under the License.
package rollout
import (
"context"
"fmt"
"strconv"
"time"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/pkg/trafficrouting"
"github.com/openkruise/rollouts/pkg/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
var defaultGracePeriodSeconds int32 = 3
type RolloutContext struct {
Rollout *v1alpha1.Rollout
NewStatus *v1alpha1.RolloutStatus
// related workload
Workload *util.Workload
// reconcile RequeueAfter recheckTime
RecheckTime *time.Time
// wait stable workload pods ready
WaitReady bool
}
// parameter1 retryReconcile, parameter2 error
func (r *RolloutReconciler) reconcileRolloutProgressing(rollout *v1alpha1.Rollout, newStatus *v1alpha1.RolloutStatus) (*time.Time, error) {
cond := util.GetRolloutCondition(rollout.Status, v1alpha1.RolloutConditionProgressing)
@ -46,7 +62,7 @@ func (r *RolloutReconciler) reconcileRolloutProgressing(rollout *v1alpha1.Rollou
klog.Infof("rollout(%s/%s) workload status is inconsistent, then wait a moment", rollout.Namespace, rollout.Name)
return nil, nil
}
rolloutContext := &util.RolloutContext{Rollout: rollout, NewStatus: newStatus, Workload: workload}
rolloutContext := &RolloutContext{Rollout: rollout, NewStatus: newStatus, Workload: workload}
switch cond.Reason {
case v1alpha1.ProgressingReasonInitializing:
klog.Infof("rollout(%s/%s) is Progressing, and in reason(%s)", rollout.Namespace, rollout.Name, cond.Reason)
@ -132,10 +148,10 @@ func (r *RolloutReconciler) reconcileRolloutProgressing(rollout *v1alpha1.Rollou
return rolloutContext.RecheckTime, nil
}
func (r *RolloutReconciler) doProgressingInitializing(c *util.RolloutContext) (bool, error) {
func (r *RolloutReconciler) doProgressingInitializing(c *RolloutContext) (bool, error) {
// Traffic routing
if len(c.Rollout.Spec.Strategy.Canary.TrafficRoutings) > 0 {
if err := r.trafficRoutingManager.InitializeTrafficRouting(c); err != nil {
if err := r.trafficRoutingManager.InitializeTrafficRouting(newTrafficRoutingContext(c)); err != nil {
return false, err
}
}
@ -149,10 +165,15 @@ func (r *RolloutReconciler) doProgressingInitializing(c *util.RolloutContext) (b
klog.Infof("verify rollout(%s/%s) TrafficRouting, and wait a moment", c.Rollout.Namespace, c.Rollout.Name)
return false, nil
}
// TrafficRouting indicates the gateway traffic routing, and rollout release will trigger the trafficRouting
if c.Rollout.Annotations[v1alpha1.TrafficRoutingAnnotation] != "" {
return r.handleTrafficRouting(c.Rollout.Namespace, c.Rollout.Name, c.Rollout.Annotations[v1alpha1.TrafficRoutingAnnotation])
}
return true, nil
}
func (r *RolloutReconciler) doProgressingInRolling(c *util.RolloutContext) error {
func (r *RolloutReconciler) doProgressingInRolling(c *RolloutContext) error {
// Handle the 5 special cases firstly, and we had better keep the order of following cases:
switch {
@ -185,7 +206,7 @@ func (r *RolloutReconciler) handleRolloutPaused(rollout *v1alpha1.Rollout, newSt
return nil
}
func (r *RolloutReconciler) handleContinuousRelease(c *util.RolloutContext) error {
func (r *RolloutReconciler) handleContinuousRelease(c *RolloutContext) error {
r.Recorder.Eventf(c.Rollout, corev1.EventTypeNormal, "Progressing", "workload continuous publishing canaryRevision, then restart publishing")
klog.Infof("rollout(%s/%s) workload continuous publishing canaryRevision from(%s) -> to(%s), then restart publishing",
c.Rollout.Namespace, c.Rollout.Name, c.NewStatus.CanaryStatus.CanaryRevision, c.Workload.CanaryRevision)
@ -226,7 +247,7 @@ func (r *RolloutReconciler) handleRollbackInBatches(rollout *v1alpha1.Rollout, w
return nil
}
func (r *RolloutReconciler) handleRolloutPlanChanged(c *util.RolloutContext) error {
func (r *RolloutReconciler) handleRolloutPlanChanged(c *RolloutContext) error {
newStepIndex, err := r.recalculateCanaryStep(c)
if err != nil {
klog.Errorf("rollout(%s/%s) reCalculate Canary StepIndex failed: %s", c.Rollout.Namespace, c.Rollout.Name, err.Error())
@ -242,13 +263,62 @@ func (r *RolloutReconciler) handleRolloutPlanChanged(c *util.RolloutContext) err
return nil
}
func (r *RolloutReconciler) handleNormalRolling(c *util.RolloutContext) error {
//check if canary is done
func (r *RolloutReconciler) handleNormalRolling(c *RolloutContext) error {
// check if canary is done
if c.NewStatus.CanaryStatus.CurrentStepState == v1alpha1.CanaryStepStateCompleted {
klog.Infof("rollout(%s/%s) progressing rolling done", c.Rollout.Namespace, c.Rollout.Name)
progressingStateTransition(c.NewStatus, corev1.ConditionTrue, v1alpha1.ProgressingReasonFinalising, "Rollout has been completed and some closing work is being done")
} else { // rollout is in rolling
return r.canaryManager.runCanary(c)
return nil
}
return r.canaryManager.runCanary(c)
}
// name is rollout name, tr is trafficRouting name
func (r *RolloutReconciler) handleTrafficRouting(namespace, name, tr string) (bool, error) {
obj := &v1alpha1.TrafficRouting{}
err := r.Get(context.TODO(), client.ObjectKey{Namespace: namespace, Name: tr}, obj)
if err != nil {
if errors.IsNotFound(err) {
klog.Warningf("rollout(%s/%s) trafficRouting(%s) Not Found, and wait a moment", namespace, name, tr)
return false, nil
}
return false, err
}
if controllerutil.ContainsFinalizer(obj, util.ProgressingRolloutFinalizer(name)) {
return true, nil
}
if obj.Status.Phase == v1alpha1.TrafficRoutingPhaseFinalizing || obj.Status.Phase == v1alpha1.TrafficRoutingPhaseTerminating {
klog.Infof("rollout(%s/%s) trafficRouting(%s) phase(%s), and wait a moment", namespace, name, tr, obj.Status.Phase)
return false, nil
}
err = util.UpdateFinalizer(r.Client, obj, util.AddFinalizerOpType, util.ProgressingRolloutFinalizer(name))
if err != nil {
klog.Errorf("rollout(%s/%s) add trafficRouting(%s) finalizer failed: %s", namespace, name, tr, err.Error())
return false, err
}
klog.Infof("rollout(%s/%s) add trafficRouting(%s) finalizer(%s) success", namespace, name, tr, util.ProgressingRolloutFinalizer(name))
return false, nil
}
func (r *RolloutReconciler) finalizeTrafficRouting(namespace, name, tr string) error {
obj := &v1alpha1.TrafficRouting{}
err := r.Get(context.TODO(), client.ObjectKey{Namespace: namespace, Name: tr}, obj)
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
// progressing.rollouts.kruise.io/rollout-b
finalizer := util.ProgressingRolloutFinalizer(name)
if controllerutil.ContainsFinalizer(obj, finalizer) {
err = util.UpdateFinalizer(r.Client, obj, util.RemoveFinalizerOpType, finalizer)
if err != nil {
klog.Errorf("rollout(%s/%s) remove trafficRouting(%s) finalizer failed: %s", namespace, name, tr, err.Error())
return err
}
klog.Infof("rollout(%s/%s) remove trafficRouting(%s) remove finalizer(%s) success", namespace, name, tr, finalizer)
return nil
}
return nil
}
@ -288,10 +358,12 @@ func isRollingBackInBatches(rollout *v1alpha1.Rollout, workload *util.Workload)
// 1. modify network api(ingress or gateway api) configuration, and route 100% traffic to stable pods
// 2. remove batchRelease CR.
func (r *RolloutReconciler) doProgressingReset(c *util.RolloutContext) (bool, error) {
func (r *RolloutReconciler) doProgressingReset(c *RolloutContext) (bool, error) {
if len(c.Rollout.Spec.Strategy.Canary.TrafficRoutings) > 0 {
// modify network api(ingress or gateway api) configuration, and route 100% traffic to stable pods
done, err := r.trafficRoutingManager.FinalisingTrafficRouting(c, false)
tr := newTrafficRoutingContext(c)
done, err := r.trafficRoutingManager.FinalisingTrafficRouting(tr, false)
c.NewStatus.CanaryStatus.LastUpdateTime = tr.LastUpdateTime
if err != nil || !done {
return done, err
}
@ -306,7 +378,7 @@ func (r *RolloutReconciler) doProgressingReset(c *util.RolloutContext) (bool, er
return true, nil
}
func (r *RolloutReconciler) recalculateCanaryStep(c *util.RolloutContext) (int32, error) {
func (r *RolloutReconciler) recalculateCanaryStep(c *RolloutContext) (int32, error) {
batch, err := r.canaryManager.fetchBatchRelease(c.Rollout.Namespace, c.Rollout.Name)
if errors.IsNotFound(err) {
return 1, nil
@ -332,8 +404,15 @@ func (r *RolloutReconciler) recalculateCanaryStep(c *util.RolloutContext) (int32
return stepIndex, nil
}
func (r *RolloutReconciler) doFinalising(c *util.RolloutContext) (bool, error) {
func (r *RolloutReconciler) doFinalising(c *RolloutContext) (bool, error) {
klog.Infof("reconcile rollout(%s/%s) doFinalising", c.Rollout.Namespace, c.Rollout.Name)
// TrafficRouting indicates the gateway traffic routing, and rollout finalizer will trigger the trafficRouting finalizer
if c.Rollout.Annotations[v1alpha1.TrafficRoutingAnnotation] != "" {
err := r.finalizeTrafficRouting(c.Rollout.Namespace, c.Rollout.Name, c.Rollout.Annotations[v1alpha1.TrafficRoutingAnnotation])
if err != nil {
return false, err
}
}
done, err := r.canaryManager.doCanaryFinalising(c)
if err != nil {
klog.Errorf("rollout(%s/%s) Progressing failed: %s", c.Rollout.Namespace, c.Rollout.Name, err.Error())
@ -370,3 +449,22 @@ func setRolloutSucceededCondition(status *v1alpha1.RolloutStatus, condStatus cor
}
util.SetRolloutCondition(status, *cond)
}
func newTrafficRoutingContext(c *RolloutContext) *trafficrouting.TrafficRoutingContext {
currentStep := c.Rollout.Spec.Strategy.Canary.Steps[c.NewStatus.CanaryStatus.CurrentStepIndex-1]
var revisionLabelKey string
if c.Workload != nil {
revisionLabelKey = c.Workload.RevisionLabelKey
}
return &trafficrouting.TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", c.Rollout.Namespace, c.Rollout.Name),
Namespace: c.Rollout.Namespace,
ObjectRef: c.Rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(c.Rollout, rolloutControllerKind),
RevisionLabelKey: revisionLabelKey,
StableRevision: c.NewStatus.CanaryStatus.StableRevision,
CanaryRevision: c.NewStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: c.NewStatus.CanaryStatus.LastUpdateTime,
}
}

View File

@ -40,9 +40,41 @@ func TestReconcileRolloutProgressing(t *testing.T) {
name string
getObj func() ([]*apps.Deployment, []*apps.ReplicaSet)
getNetwork func() ([]*corev1.Service, []*netv1.Ingress)
getRollout func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease)
getRollout func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting)
expectStatus func() *v1alpha1.RolloutStatus
expectTr func() *v1alpha1.TrafficRouting
}{
{
name: "ReconcileRolloutProgressing init trafficRouting",
getObj: func() ([]*apps.Deployment, []*apps.ReplicaSet) {
dep1 := deploymentDemo.DeepCopy()
rs1 := rsDemo.DeepCopy()
return []*apps.Deployment{dep1}, []*apps.ReplicaSet{rs1}
},
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Annotations[v1alpha1.TrafficRoutingAnnotation] = "tr-demo"
return obj, nil, demoTR.DeepCopy()
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
s.CanaryStatus.ObservedWorkloadGeneration = 2
s.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
s.CanaryStatus.StableRevision = "pod-template-hash-v1"
s.CanaryStatus.CanaryRevision = "56855c89f9"
s.CanaryStatus.CurrentStepIndex = 1
s.CanaryStatus.CurrentStepState = v1alpha1.CanaryStepStateUpgrade
return s
},
expectTr: func() *v1alpha1.TrafficRouting {
tr := demoTR.DeepCopy()
tr.Finalizers = []string{util.ProgressingRolloutFinalizer(rolloutDemo.Name)}
return tr
},
},
{
name: "ReconcileRolloutProgressing init -> rolling",
getObj: func() ([]*apps.Deployment, []*apps.ReplicaSet) {
@ -53,9 +85,11 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
return obj, nil
tr := demoTR.DeepCopy()
tr.Finalizers = []string{util.ProgressingRolloutFinalizer(rolloutDemo.Name)}
return obj, nil, tr
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -98,7 +132,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -109,7 +143,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
cond := util.GetRolloutCondition(obj.Status, v1alpha1.RolloutConditionProgressing)
cond.Reason = v1alpha1.ProgressingReasonInRolling
util.SetRolloutCondition(&obj.Status, *cond)
return obj, nil
return obj, nil, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -153,7 +187,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -165,7 +199,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
cond := util.GetRolloutCondition(obj.Status, v1alpha1.RolloutConditionProgressing)
cond.Reason = v1alpha1.ProgressingReasonInRolling
util.SetRolloutCondition(&obj.Status, *cond)
return obj, nil
return obj, nil, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -201,8 +235,9 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Annotations[v1alpha1.TrafficRoutingAnnotation] = "tr-demo"
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
obj.Status.CanaryStatus.StableRevision = "pod-template-hash-v1"
@ -220,7 +255,9 @@ func TestReconcileRolloutProgressing(t *testing.T) {
CanaryReplicas: intstr.FromInt(1),
},
}
return obj, br
tr := demoTR.DeepCopy()
tr.Finalizers = []string{util.ProgressingRolloutFinalizer(rolloutDemo.Name)}
return obj, br, tr
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -237,6 +274,10 @@ func TestReconcileRolloutProgressing(t *testing.T) {
util.SetRolloutCondition(s, *cond)
return s
},
expectTr: func() *v1alpha1.TrafficRouting {
tr := demoTR.DeepCopy()
return tr
},
},
{
name: "ReconcileRolloutProgressing finalizing2",
@ -256,7 +297,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -276,7 +317,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
},
}
br.Status.Phase = v1alpha1.RolloutPhaseCompleted
return obj, br
return obj, br, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -312,7 +353,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -325,7 +366,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
cond.Reason = v1alpha1.ProgressingReasonFinalising
cond.Status = corev1.ConditionTrue
util.SetRolloutCondition(&obj.Status, *cond)
return obj, nil
return obj, nil, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -348,7 +389,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
},
},
{
name: "ReconcileRolloutProgressing rolling -> rollback",
name: "ReconcileRolloutProgressing rolling -> rollback1",
getObj: func() ([]*apps.Deployment, []*apps.ReplicaSet) {
dep1 := deploymentDemo.DeepCopy()
dep1.Spec.Template.Spec.Containers[0].Image = "echoserver:v1"
@ -375,7 +416,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -387,7 +428,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
cond := util.GetRolloutCondition(obj.Status, v1alpha1.RolloutConditionProgressing)
cond.Reason = v1alpha1.ProgressingReasonInRolling
util.SetRolloutCondition(&obj.Status, *cond)
return obj, nil
return obj, nil, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -405,7 +446,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
},
},
{
name: "ReconcileRolloutProgressing rolling -> rollback",
name: "ReconcileRolloutProgressing rolling -> rollback2",
getObj: func() ([]*apps.Deployment, []*apps.ReplicaSet) {
dep1 := deploymentDemo.DeepCopy()
dep1.Spec.Template.Spec.Containers[0].Image = "echoserver:v1"
@ -432,7 +473,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -444,7 +485,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
cond := util.GetRolloutCondition(obj.Status, v1alpha1.RolloutConditionProgressing)
cond.Reason = v1alpha1.ProgressingReasonInRolling
util.SetRolloutCondition(&obj.Status, *cond)
return obj, nil
return obj, nil, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -489,7 +530,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
getNetwork: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease) {
getRollout: func() (*v1alpha1.Rollout, *v1alpha1.BatchRelease, *v1alpha1.TrafficRouting) {
obj := rolloutDemo.DeepCopy()
obj.Status.CanaryStatus.ObservedWorkloadGeneration = 2
obj.Status.CanaryStatus.RolloutHash = "f55bvd874d5f2fzvw46bv966x4bwbdv4wx6bd9f7b46ww788954b8z8w29b7wxfd"
@ -503,7 +544,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
cond := util.GetRolloutCondition(obj.Status, v1alpha1.RolloutConditionProgressing)
cond.Reason = v1alpha1.ProgressingReasonInRolling
util.SetRolloutCondition(&obj.Status, *cond)
return obj, nil
return obj, nil, nil
},
expectStatus: func() *v1alpha1.RolloutStatus {
s := rolloutDemo.Status.DeepCopy()
@ -519,7 +560,7 @@ func TestReconcileRolloutProgressing(t *testing.T) {
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
deps, rss := cs.getObj()
rollout, br := cs.getRollout()
rollout, br, tr := cs.getRollout()
fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(rollout, demoConf.DeepCopy()).Build()
for _, rs := range rss {
_ = fc.Create(context.TODO(), rs)
@ -530,6 +571,9 @@ func TestReconcileRolloutProgressing(t *testing.T) {
if br != nil {
_ = fc.Create(context.TODO(), br)
}
if tr != nil {
_ = fc.Create(context.TODO(), tr)
}
ss, in := cs.getNetwork()
for _, obj := range ss {
_ = fc.Create(context.TODO(), obj)
@ -556,6 +600,17 @@ func TestReconcileRolloutProgressing(t *testing.T) {
}
_ = r.updateRolloutStatusInternal(rollout, *newStatus)
checkRolloutEqual(fc, t, client.ObjectKey{Name: rollout.Name}, cs.expectStatus())
if cs.expectTr != nil {
expectTr := cs.expectTr()
obj := &v1alpha1.TrafficRouting{}
err = fc.Get(context.TODO(), client.ObjectKey{Name: expectTr.Name}, obj)
if err != nil {
t.Fatalf("get object failed: %s", err.Error())
}
if !reflect.DeepEqual(obj.Finalizers, expectTr.Finalizers) {
t.Fatalf("expect(%s), but get(%s)", expectTr.Finalizers, obj.Finalizers)
}
}
})
}
}
@ -604,13 +659,19 @@ func TestReCalculateCanaryStepIndex(t *testing.T) {
obj := rolloutDemo.DeepCopy()
obj.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
},
{
Weight: utilpointer.Int32(50),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(50),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
},
}
return obj
@ -643,13 +704,19 @@ func TestReCalculateCanaryStepIndex(t *testing.T) {
obj := rolloutDemo.DeepCopy()
obj.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
},
{
Weight: utilpointer.Int32(40),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
},
}
return obj
@ -682,13 +749,19 @@ func TestReCalculateCanaryStepIndex(t *testing.T) {
obj := rolloutDemo.DeepCopy()
obj.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(40),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
},
}
return obj
@ -721,13 +794,19 @@ func TestReCalculateCanaryStepIndex(t *testing.T) {
obj := rolloutDemo.DeepCopy()
obj.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(10),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(30),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(30),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
},
}
return obj
@ -760,14 +839,18 @@ func TestReCalculateCanaryStepIndex(t *testing.T) {
obj := rolloutDemo.DeepCopy()
obj.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(2),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(2),
},
Replicas: &intstr.IntOrString{
Type: intstr.String,
StrVal: "10%",
},
},
{
Weight: utilpointer.Int32(3),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(3),
},
Replicas: &intstr.IntOrString{
Type: intstr.String,
StrVal: "10%",
@ -819,7 +902,7 @@ func TestReCalculateCanaryStepIndex(t *testing.T) {
if err != nil {
t.Fatalf(err.Error())
}
c := &util.RolloutContext{Rollout: rollout, Workload: workload}
c := &RolloutContext{Rollout: rollout, Workload: workload}
newStepIndex, err := reconciler.recalculateCanaryStep(c)
if err != nil {
t.Fatalf(err.Error())

View File

@ -66,6 +66,8 @@ func (r *RolloutReconciler) calculateRolloutStatus(rollout *v1alpha1.Rollout) (r
klog.Infof("rollout(%s/%s) workload not found, and reset status be Initial", rollout.Namespace, rollout.Name)
return false, newStatus, nil
}
klog.V(5).Infof("rollout(%s/%s) workload(%s)", rollout.Namespace, rollout.Name, util.DumpJSON(workload))
// todo, patch workload webhook labels
// workload status generation is not equal to workload.generation
if !workload.IsStatusConsistent {
klog.Infof("rollout(%s/%s) workload status is inconsistent, then wait a moment", rollout.Namespace, rollout.Name)
@ -187,7 +189,7 @@ func (r *RolloutReconciler) reconcileRolloutTerminating(rollout *v1alpha1.Rollou
klog.Errorf("rollout(%s/%s) get workload failed: %s", rollout.Namespace, rollout.Name, err.Error())
return nil, err
}
c := &util.RolloutContext{Rollout: rollout, NewStatus: newStatus, Workload: workload}
c := &RolloutContext{Rollout: rollout, NewStatus: newStatus, Workload: workload}
done, err := r.doFinalising(c)
if err != nil {
return nil, err

View File

@ -63,10 +63,14 @@ func TestCalculateRolloutHash(t *testing.T) {
obj := rolloutDemo.DeepCopy()
obj.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(50),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(50),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
},
}
return obj

View File

@ -137,23 +137,29 @@ var (
Canary: &rolloutv1alpha1.CanaryStrategy{
Steps: []rolloutv1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(5),
TrafficRoutingStrategy: rolloutv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(5),
},
Pause: rolloutv1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
},
{
Weight: utilpointer.Int32(40),
Pause: rolloutv1alpha1.RolloutPause{},
TrafficRoutingStrategy: rolloutv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: rolloutv1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: rolloutv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: rolloutv1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
},
},
TrafficRoutings: []*rolloutv1alpha1.TrafficRouting{
TrafficRoutings: []rolloutv1alpha1.TrafficRoutingRef{
{
Service: "service-demo",
Ingress: &rolloutv1alpha1.IngressTrafficRouting{

View File

@ -0,0 +1,225 @@
/*
Copyright 2023 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 trafficrouting
import (
"context"
"flag"
"fmt"
"reflect"
"strings"
"time"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/pkg/trafficrouting"
"github.com/openkruise/rollouts/pkg/util"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"
)
var (
concurrentReconciles = 3
defaultGracePeriodSeconds int32 = 3
trControllerKind = v1alpha1.SchemeGroupVersion.WithKind("TrafficRouting")
)
func init() {
flag.IntVar(&concurrentReconciles, "trafficrouting-workers", 3, "Max concurrent workers for trafficrouting controller.")
}
// TrafficRoutingReconciler reconciles a TrafficRouting object
type TrafficRoutingReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
trafficRoutingManager *trafficrouting.Manager
}
//+kubebuilder:rbac:groups=rollouts.kruise.io,resources=trafficroutings,verbs=get;list;watch;create;update;patch
//+kubebuilder:rbac:groups=rollouts.kruise.io,resources=trafficroutings/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=rollouts.kruise.io,resources=trafficroutings/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Rollout object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile
func (r *TrafficRoutingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Fetch the Rollout instance
tr := &v1alpha1.TrafficRouting{}
err := r.Get(context.TODO(), req.NamespacedName, tr)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return ctrl.Result{}, nil
}
// Error reading the object - requeue the request.
return ctrl.Result{}, err
}
klog.Infof("Begin to reconcile TrafficRouting %v", util.DumpJSON(tr))
// handle finalizer
err = r.handleFinalizer(tr)
if err != nil {
return ctrl.Result{}, err
}
newStatus := tr.Status.DeepCopy()
if newStatus.Phase == "" {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseInitial
}
if !tr.DeletionTimestamp.IsZero() {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseTerminating
}
var done = true
switch newStatus.Phase {
case v1alpha1.TrafficRoutingPhaseInitial:
err = r.trafficRoutingManager.InitializeTrafficRouting(newTrafficRoutingContext(tr))
if err == nil {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseHealthy
newStatus.Message = "TrafficRouting is Healthy"
}
case v1alpha1.TrafficRoutingPhaseHealthy:
if rolloutProgressingFinalizer(tr).Len() > 0 {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseProgressing
newStatus.Message = "TrafficRouting is Progressing"
}
case v1alpha1.TrafficRoutingPhaseProgressing:
if rolloutProgressingFinalizer(tr).Len() == 0 {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseFinalizing
newStatus.Message = "TrafficRouting is Finalizing"
} else {
done, err = r.trafficRoutingManager.DoTrafficRouting(newTrafficRoutingContext(tr))
}
case v1alpha1.TrafficRoutingPhaseFinalizing:
done, err = r.trafficRoutingManager.FinalisingTrafficRouting(newTrafficRoutingContext(tr), false)
if done {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseHealthy
newStatus.Message = "TrafficRouting is Healthy"
}
case v1alpha1.TrafficRoutingPhaseTerminating:
done, err = r.trafficRoutingManager.FinalisingTrafficRouting(newTrafficRoutingContext(tr), false)
if done {
// remove trafficRouting finalizer
err = r.handleFinalizer(tr)
}
}
if err != nil {
return ctrl.Result{}, err
} else if !done {
recheckTime := time.Now().Add(time.Duration(defaultGracePeriodSeconds) * time.Second)
return ctrl.Result{RequeueAfter: time.Until(recheckTime)}, nil
}
newStatus.ObservedGeneration = tr.Generation
return ctrl.Result{}, r.updateTrafficRoutingStatus(tr, *newStatus)
}
func (r *TrafficRoutingReconciler) updateTrafficRoutingStatus(tr *v1alpha1.TrafficRouting, newStatus v1alpha1.TrafficRoutingStatus) error {
if reflect.DeepEqual(tr.Status, newStatus) {
return nil
}
var err error
trClone := tr.DeepCopy()
if err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: tr.Namespace, Name: tr.Name}, trClone); err != nil {
klog.Errorf("error getting updated trafficRouting(%s/%s) from client", tr.Namespace, tr.Name)
return err
}
trClone.Status = newStatus
if err = r.Client.Status().Update(context.TODO(), trClone); err != nil {
klog.Errorf("update trafficRouting(%s/%s) status failed: %s", tr.Namespace, tr.Name, err.Error())
return err
}
klog.Infof("trafficRouting(%s/%s) status from(%s) -> to(%s) success", tr.Namespace, tr.Name, util.DumpJSON(tr.Status), util.DumpJSON(newStatus))
return nil
}
// handle adding and handle finalizer logic, it turns if we should continue to reconcile
func (r *TrafficRoutingReconciler) handleFinalizer(tr *v1alpha1.TrafficRouting) error {
// delete trafficRouting crd, remove finalizer
if !tr.DeletionTimestamp.IsZero() {
err := util.UpdateFinalizer(r.Client, tr, util.RemoveFinalizerOpType, util.TrafficRoutingFinalizer)
if err != nil {
klog.Errorf("remove trafficRouting(%s/%s) finalizer failed: %s", tr.Namespace, tr.Name, err.Error())
return err
}
klog.Infof("remove trafficRouting(%s/%s) finalizer success", tr.Namespace, tr.Name)
return nil
}
// create trafficRouting crd, add finalizer
if !controllerutil.ContainsFinalizer(tr, util.TrafficRoutingFinalizer) {
err := util.UpdateFinalizer(r.Client, tr, util.AddFinalizerOpType, util.TrafficRoutingFinalizer)
if err != nil {
klog.Errorf("register trafficRouting(%s/%s) finalizer failed: %s", tr.Namespace, tr.Name, err.Error())
return err
}
klog.Infof("register trafficRouting(%s/%s) finalizer success", tr.Namespace, tr.Name)
}
return nil
}
func rolloutProgressingFinalizer(tr *v1alpha1.TrafficRouting) sets.String {
progressing := sets.String{}
for _, s := range tr.GetFinalizers() {
if strings.Contains(s, v1alpha1.ProgressingRolloutFinalizerPrefix) {
progressing.Insert(s)
}
}
return progressing
}
func (r *TrafficRoutingReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Create a new controller
c, err := controller.New("trafficrouting-controller", mgr, controller.Options{
Reconciler: r, MaxConcurrentReconciles: concurrentReconciles})
if err != nil {
return err
}
// Watch for changes to trafficrouting
if err = c.Watch(&source.Kind{Type: &v1alpha1.TrafficRouting{}}, &handler.EnqueueRequestForObject{}); err != nil {
return err
}
r.trafficRoutingManager = trafficrouting.NewTrafficRoutingManager(mgr.GetClient())
return nil
}
func newTrafficRoutingContext(tr *v1alpha1.TrafficRouting) *trafficrouting.TrafficRoutingContext {
return &trafficrouting.TrafficRoutingContext{
Key: fmt.Sprintf("TrafficRouting(%s/%s)", tr.Namespace, tr.Name),
Namespace: tr.Namespace,
ObjectRef: tr.Spec.ObjectRef,
Strategy: tr.Spec.Strategy,
OwnerRef: *metav1.NewControllerRef(tr, trControllerKind),
OnlyTrafficRouting: true,
}
}

View File

@ -0,0 +1,427 @@
/*
Copyright 2023 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 trafficrouting
import (
"context"
"fmt"
"reflect"
"testing"
kruisev1aplphal "github.com/openkruise/kruise-api/apps/v1alpha1"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/pkg/trafficrouting"
"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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"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
nginxIngressAnnotationDefaultPrefix = "nginx.ingress.kubernetes.io"
demoService = corev1.Service{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: metav1.ObjectMeta{
Name: "echoserver",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: 80,
TargetPort: intstr.FromInt(8080),
},
},
Selector: map[string]string{
"app": "echoserver",
},
},
}
demoIngress = netv1.Ingress{
TypeMeta: metav1.TypeMeta{
APIVersion: "networking.k8s.io/v1",
Kind: "Ingress",
},
ObjectMeta: metav1.ObjectMeta{
Name: "echoserver",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "nginx",
},
},
Spec: netv1.IngressSpec{
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{
Name: "http",
},
},
},
},
},
},
},
},
},
},
}
demoTR = &v1alpha1.TrafficRouting{
TypeMeta: metav1.TypeMeta{
APIVersion: "rollouts.kruise.io/v1alpha1",
Kind: "TrafficRouting",
},
ObjectMeta: metav1.ObjectMeta{
Name: "tr-demo",
Labels: map[string]string{},
},
Spec: v1alpha1.TrafficRoutingSpec{
ObjectRef: []v1alpha1.TrafficRoutingRef{
{
Service: "echoserver",
Ingress: &v1alpha1.IngressTrafficRouting{
Name: "echoserver",
},
},
},
Strategy: v1alpha1.TrafficRoutingStrategy{
Matches: []v1alpha1.HttpRouteMatch{
// header
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "user_id",
Value: "123456",
},
},
},
},
},
},
}
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
`,
},
}
)
func init() {
scheme = runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = kruisev1aplphal.AddToScheme(scheme)
_ = v1alpha1.AddToScheme(scheme)
}
func TestTrafficRoutingTest(t *testing.T) {
cases := []struct {
name string
getObj func() ([]*corev1.Service, []*netv1.Ingress)
getTrafficRouting func() *v1alpha1.TrafficRouting
expectObj func() ([]*corev1.Service, []*netv1.Ingress, *v1alpha1.TrafficRouting)
expectDone bool
}{
{
name: "TrafficRouting reconcile Initial->Healthy",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getTrafficRouting: func() *v1alpha1.TrafficRouting {
return demoTR.DeepCopy()
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress, *v1alpha1.TrafficRouting) {
s1 := demoService.DeepCopy()
tr := demoTR.DeepCopy()
tr.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseHealthy,
}
return []*corev1.Service{s1}, []*netv1.Ingress{demoIngress.DeepCopy()}, tr
},
expectDone: true,
},
{
name: "TrafficRouting reconcile Initial->Progressing",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getTrafficRouting: func() *v1alpha1.TrafficRouting {
obj := demoTR.DeepCopy()
obj.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseHealthy,
}
obj.Finalizers = []string{fmt.Sprintf("%s/rollout-test", v1alpha1.ProgressingRolloutFinalizerPrefix),
util.TrafficRoutingFinalizer}
return obj
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress, *v1alpha1.TrafficRouting) {
s1 := demoService.DeepCopy()
tr := demoTR.DeepCopy()
tr.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
return []*corev1.Service{s1}, []*netv1.Ingress{demoIngress.DeepCopy()}, tr
},
expectDone: true,
},
{
name: "TrafficRouting reconcile Progressing, and create canary ingress",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getTrafficRouting: func() *v1alpha1.TrafficRouting {
obj := demoTR.DeepCopy()
obj.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
obj.Finalizers = []string{fmt.Sprintf("%s/rollout-test", v1alpha1.ProgressingRolloutFinalizerPrefix),
util.TrafficRoutingFinalizer}
return obj
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress, *v1alpha1.TrafficRouting) {
s1 := demoService.DeepCopy()
i1 := demoIngress.DeepCopy()
i2 := demoIngress.DeepCopy()
i2.Name = "echoserver-canary"
i2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
i2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "0"
tr := demoTR.DeepCopy()
tr.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
return []*corev1.Service{s1}, []*netv1.Ingress{i1, i2}, tr
},
expectDone: false,
},
{
name: "TrafficRouting reconcile Progressing, and set ingress headers, and return false",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
i1 := demoIngress.DeepCopy()
i2 := demoIngress.DeepCopy()
i2.Name = "echoserver-canary"
i2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
i2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "0"
return []*corev1.Service{s1}, []*netv1.Ingress{i1, i2}
},
getTrafficRouting: func() *v1alpha1.TrafficRouting {
obj := demoTR.DeepCopy()
obj.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
obj.Finalizers = []string{fmt.Sprintf("%s/rollout-test", v1alpha1.ProgressingRolloutFinalizerPrefix),
util.TrafficRoutingFinalizer}
return obj
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress, *v1alpha1.TrafficRouting) {
s1 := demoService.DeepCopy()
i1 := demoIngress.DeepCopy()
i2 := demoIngress.DeepCopy()
i2.Name = "echoserver-canary"
i2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
i2.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
i2.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
tr := demoTR.DeepCopy()
tr.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
return []*corev1.Service{s1}, []*netv1.Ingress{i1, i2}, tr
},
expectDone: false,
},
{
name: "TrafficRouting reconcile Progressing, and set ingress headers, and return true",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
i1 := demoIngress.DeepCopy()
i2 := demoIngress.DeepCopy()
i2.Name = "echoserver-canary"
i2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
i2.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
i2.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
return []*corev1.Service{s1}, []*netv1.Ingress{i1, i2}
},
getTrafficRouting: func() *v1alpha1.TrafficRouting {
obj := demoTR.DeepCopy()
obj.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
obj.Finalizers = []string{fmt.Sprintf("%s/rollout-test", v1alpha1.ProgressingRolloutFinalizerPrefix),
util.TrafficRoutingFinalizer}
return obj
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress, *v1alpha1.TrafficRouting) {
s1 := demoService.DeepCopy()
i1 := demoIngress.DeepCopy()
i2 := demoIngress.DeepCopy()
i2.Name = "echoserver-canary"
i2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
i2.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id"
i2.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456"
tr := demoTR.DeepCopy()
tr.Status = v1alpha1.TrafficRoutingStatus{
Phase: v1alpha1.TrafficRoutingPhaseProgressing,
}
return []*corev1.Service{s1}, []*netv1.Ingress{i1, i2}, tr
},
expectDone: true,
},
}
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
ss, ig := cs.getObj()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ig[0], ss[0], demoConf.DeepCopy()).Build()
if len(ss) == 2 {
_ = client.Create(context.TODO(), ss[1])
}
if len(ig) == 2 {
_ = client.Create(context.TODO(), ig[1])
}
tr := cs.getTrafficRouting()
_ = client.Create(context.TODO(), tr)
manager := TrafficRoutingReconciler{
Client: client,
Scheme: scheme,
trafficRoutingManager: trafficrouting.NewTrafficRoutingManager(client),
}
result, err := manager.Reconcile(context.TODO(), ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: tr.Namespace,
Name: tr.Name,
},
})
if err != nil {
t.Fatalf("TrafficRouting Reconcile failed: %s", err)
}
if cs.expectDone != (result.RequeueAfter == 0) {
t.Fatalf("TrafficRouting Reconcile expect(%v), but get(%v)", cs.expectDone, result.RequeueAfter)
}
ss, ig, tr = cs.expectObj()
checkObjEqual(client, t, tr)
for _, obj := range ss {
checkObjEqual(client, t, obj)
}
for _, obj := range ig {
checkObjEqual(client, t, obj)
}
})
}
}
func checkObjEqual(c client.WithWatch, t *testing.T, expect client.Object) {
gvk := expect.GetObjectKind().GroupVersionKind()
obj := getEmptyObject(gvk)
err := c.Get(context.TODO(), client.ObjectKey{Namespace: expect.GetNamespace(), Name: expect.GetName()}, obj)
if err != nil {
t.Fatalf("get object failed: %s", err.Error())
}
switch gvk.Kind {
case "Service":
s1 := obj.(*corev1.Service)
s2 := expect.(*corev1.Service)
if !reflect.DeepEqual(s1.Spec, s2.Spec) {
t.Fatalf("expect(%s), but get object(%s)", util.DumpJSON(s2.Spec), util.DumpJSON(s1.Spec))
}
case "Ingress":
s1 := obj.(*netv1.Ingress)
s2 := expect.(*netv1.Ingress)
if !reflect.DeepEqual(s1.Spec, s2.Spec) || !reflect.DeepEqual(s1.Annotations, s2.Annotations) {
t.Fatalf("expect(%s), but get object(%s)", util.DumpJSON(s2), util.DumpJSON(s1))
}
case "TrafficRouting":
s1 := obj.(*v1alpha1.TrafficRouting)
s1.Status.Message = ""
s2 := expect.(*v1alpha1.TrafficRouting)
if !reflect.DeepEqual(s1.Status, s2.Status) {
t.Fatalf("expect(%s), but get object(%s)", util.DumpJSON(s2), util.DumpJSON(s1))
}
}
}
func getEmptyObject(gvk schema.GroupVersionKind) client.Object {
switch gvk.Kind {
case "Service":
return &corev1.Service{}
case "Ingress":
return &netv1.Ingress{}
case "TrafficRouting":
return &v1alpha1.TrafficRouting{}
}
return nil
}

View File

@ -37,9 +37,27 @@ import (
var (
defaultGracePeriodSeconds int32 = 3
rolloutControllerKind = v1alpha1.SchemeGroupVersion.WithKind("Rollout")
)
type TrafficRoutingContext struct {
// only for log info
Key string
Namespace string
ObjectRef []v1alpha1.TrafficRoutingRef
Strategy v1alpha1.TrafficRoutingStrategy
// OnlyTrafficRouting
OnlyTrafficRouting bool
OwnerRef metav1.OwnerReference
// workload.RevisionLabelKey
RevisionLabelKey string
// status.CanaryStatus.StableRevision
StableRevision string
// status.CanaryStatus.PodTemplateHash
CanaryRevision string
// newStatus.canaryStatus.LastUpdateTime
LastUpdateTime *metav1.Time
}
// Manager responsible for adjusting network resources
// such as Service, Ingress, Gateway API, etc., to achieve traffic grayscale.
type Manager struct {
@ -52,48 +70,44 @@ func NewTrafficRoutingManager(c client.Client) *Manager {
// InitializeTrafficRouting determine if the network resources(service & ingress & gateway api) exist.
// If it is Ingress, init method will create the canary ingress resources, and set weight=0.
func (m *Manager) InitializeTrafficRouting(c *util.RolloutContext) error {
if len(c.Rollout.Spec.Strategy.Canary.TrafficRoutings) == 0 {
func (m *Manager) InitializeTrafficRouting(c *TrafficRoutingContext) error {
if len(c.ObjectRef) == 0 {
return nil
}
sService := c.Rollout.Spec.Strategy.Canary.TrafficRoutings[0].Service
objectRef := c.ObjectRef[0]
sService := objectRef.Service
// check service
service := &corev1.Service{}
if err := m.Get(context.TODO(), types.NamespacedName{Namespace: c.Rollout.Namespace, Name: sService}, service); err != nil {
if err := m.Get(context.TODO(), types.NamespacedName{Namespace: c.Namespace, Name: sService}, service); err != nil {
return err
}
cService := fmt.Sprintf("%s-canary", sService)
cService := getCanaryServiceName(sService, c.OnlyTrafficRouting)
// new network provider, ingress or gateway
trController, err := newNetworkProvider(m.Client, c.Rollout, c.NewStatus, sService, cService)
trController, err := newNetworkProvider(m.Client, c, sService, cService)
if err != nil {
klog.Errorf("rollout(%s/%s) newNetworkProvider failed: %s", c.Rollout.Namespace, c.Rollout.Name, err.Error())
klog.Errorf("%s newNetworkProvider failed: %s", c.Key, err.Error())
return err
}
return trController.Initialize(context.TODO())
}
func (m *Manager) DoTrafficRouting(c *util.RolloutContext) (bool, error) {
if len(c.Rollout.Spec.Strategy.Canary.TrafficRoutings) == 0 {
func (m *Manager) DoTrafficRouting(c *TrafficRoutingContext) (bool, error) {
if len(c.ObjectRef) == 0 {
return true, nil
}
trafficRouting := c.Rollout.Spec.Strategy.Canary.TrafficRoutings[0]
trafficRouting := c.ObjectRef[0]
if trafficRouting.GracePeriodSeconds <= 0 {
trafficRouting.GracePeriodSeconds = defaultGracePeriodSeconds
}
canaryStatus := c.NewStatus.CanaryStatus
currentStep := c.Rollout.Spec.Strategy.Canary.Steps[canaryStatus.CurrentStepIndex-1]
if currentStep.Weight == nil && len(currentStep.Matches) == 0 {
if c.Strategy.Weight == nil && len(c.Strategy.Matches) == 0 {
return true, nil
}
if canaryStatus.StableRevision == "" || canaryStatus.PodTemplateHash == "" {
klog.Warningf("rollout(%s/%s) stableRevision or podTemplateHash can not be empty, and wait a moment", c.Rollout.Namespace, c.Rollout.Name)
return false, nil
}
//fetch stable service
stableService := &corev1.Service{}
err := m.Get(context.TODO(), client.ObjectKey{Namespace: c.Rollout.Namespace, Name: trafficRouting.Service}, stableService)
err := m.Get(context.TODO(), client.ObjectKey{Namespace: c.Namespace, Name: trafficRouting.Service}, stableService)
if err != nil {
klog.Errorf("rollout(%s/%s) get stable service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, trafficRouting.Service, err.Error())
klog.Errorf("%s get stable service(%s) failed: %s", c.Key, trafficRouting.Service, err.Error())
// not found, wait a moment, retry
if errors.IsNotFound(err) {
return false, nil
@ -101,93 +115,99 @@ func (m *Manager) DoTrafficRouting(c *util.RolloutContext) (bool, error) {
return false, err
}
// canary service name
canaryServiceName := fmt.Sprintf("%s-canary", trafficRouting.Service)
// fetch canary service
canaryServiceName := getCanaryServiceName(trafficRouting.Service, c.OnlyTrafficRouting)
canaryService := &corev1.Service{}
err = m.Get(context.TODO(), client.ObjectKey{Namespace: c.Rollout.Namespace, Name: canaryServiceName}, canaryService)
if err != nil && !errors.IsNotFound(err) {
klog.Errorf("rollout(%s/%s) get canary service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, canaryServiceName, err.Error())
return false, err
} else if errors.IsNotFound(err) {
canaryService, err = m.createCanaryService(c, canaryServiceName, *stableService.Spec.DeepCopy())
if err != nil {
return false, err
canaryService.Namespace = stableService.Namespace
canaryService.Name = canaryServiceName
// end-to-end canary deployment scenario(a -> b -> c), if only b or c is released,
//and a is not released in this scenario, then the canary service is not needed.
if !c.OnlyTrafficRouting {
if c.StableRevision == "" || c.CanaryRevision == "" {
klog.Warningf("%s stableRevision or podTemplateHash can not be empty, and wait a moment", c.Key)
return false, nil
}
// fetch canary service
err = m.Get(context.TODO(), client.ObjectKey{Namespace: c.Namespace, Name: canaryServiceName}, canaryService)
if err != nil && !errors.IsNotFound(err) {
klog.Errorf("%s get canary service(%s) failed: %s", c.Key, canaryServiceName, err.Error())
return false, err
} else if errors.IsNotFound(err) {
canaryService, err = m.createCanaryService(c, canaryServiceName, *stableService.Spec.DeepCopy())
if err != nil {
return false, err
}
}
}
// patch canary service only selector the canary pods
if canaryService.Spec.Selector[c.Workload.RevisionLabelKey] != canaryStatus.PodTemplateHash {
body := fmt.Sprintf(`{"spec":{"selector":{"%s":"%s"}}}`, c.Workload.RevisionLabelKey, canaryStatus.PodTemplateHash)
if err = m.Patch(context.TODO(), canaryService, client.RawPatch(types.StrategicMergePatchType, []byte(body))); err != nil {
klog.Errorf("rollout(%s/%s) patch canary service(%s) selector failed: %s", c.Rollout.Namespace, c.Rollout.Name, canaryService.Name, err.Error())
return false, err
// patch canary service to only select the canary pods
if canaryService.Spec.Selector[c.RevisionLabelKey] != c.CanaryRevision {
body := fmt.Sprintf(`{"spec":{"selector":{"%s":"%s"}}}`, c.RevisionLabelKey, c.CanaryRevision)
if err = m.Patch(context.TODO(), canaryService, client.RawPatch(types.StrategicMergePatchType, []byte(body))); err != nil {
klog.Errorf("%s patch canary service(%s) selector failed: %s", c.Key, canaryService.Name, err.Error())
return false, err
}
// update canary service time, and wait 3 seconds, just to be safe
c.LastUpdateTime = &metav1.Time{Time: time.Now()}
klog.Infof("%s patch canary service(%s) selector(%s=%s) success",
c.Key, canaryService.Name, c.RevisionLabelKey, c.CanaryRevision)
}
// update canary service time, and wait 3 seconds, just to be safe
canaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now()}
klog.Infof("rollout(%s/%s) patch canary service(%s) selector(%s=%s) success",
c.Rollout.Namespace, c.Rollout.Name, canaryService.Name, c.Workload.RevisionLabelKey, canaryStatus.PodTemplateHash)
}
// patch stable service only selector the stable pods
if stableService.Spec.Selector[c.Workload.RevisionLabelKey] != canaryStatus.StableRevision {
body := fmt.Sprintf(`{"spec":{"selector":{"%s":"%s"}}}`, c.Workload.RevisionLabelKey, canaryStatus.StableRevision)
if err = m.Patch(context.TODO(), stableService, client.RawPatch(types.StrategicMergePatchType, []byte(body))); err != nil {
klog.Errorf("rollout(%s/%s) patch stable service(%s) selector failed: %s", c.Rollout.Namespace, c.Rollout.Name, stableService.Name, err.Error())
return false, err
// patch stable service to only select the stable pods
if stableService.Spec.Selector[c.RevisionLabelKey] != c.StableRevision {
body := fmt.Sprintf(`{"spec":{"selector":{"%s":"%s"}}}`, c.RevisionLabelKey, c.StableRevision)
if err = m.Patch(context.TODO(), stableService, client.RawPatch(types.StrategicMergePatchType, []byte(body))); err != nil {
klog.Errorf("%s patch stable service(%s) selector failed: %s", c.Key, stableService.Name, err.Error())
return false, err
}
// update stable service time, and wait 3 seconds, just to be safe
c.LastUpdateTime = &metav1.Time{Time: time.Now()}
klog.Infof("add %s stable service(%s) selector(%s=%s) success",
c.Key, stableService.Name, c.RevisionLabelKey, c.StableRevision)
return false, nil
}
// After modify stable service configuration, give the network provider 3 seconds to react
if verifyTime := c.LastUpdateTime.Add(time.Second * time.Duration(trafficRouting.GracePeriodSeconds)); verifyTime.After(time.Now()) {
klog.Infof("%s update service selector, and wait 3 seconds", c.Key)
return false, nil
}
// update stable service time, and wait 3 seconds, just to be safe
canaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now()}
klog.Infof("add rollout(%s/%s) stable service(%s) selector(%s=%s) success",
c.Rollout.Namespace, c.Rollout.Name, stableService.Name, c.Workload.RevisionLabelKey, canaryStatus.StableRevision)
return false, nil
}
// After modify stable service configuration, give the network provider 3 seconds to react
if verifyTime := canaryStatus.LastUpdateTime.Add(time.Second * time.Duration(trafficRouting.GracePeriodSeconds)); verifyTime.After(time.Now()) {
klog.Infof("rollout(%s/%s) update service selector, and wait 3 seconds", c.Rollout.Namespace, c.Rollout.Name)
return false, nil
}
// new network provider, ingress or gateway
trController, err := newNetworkProvider(m.Client, c.Rollout, c.NewStatus, stableService.Name, canaryService.Name)
trController, err := newNetworkProvider(m.Client, c, stableService.Name, canaryService.Name)
if err != nil {
klog.Errorf("rollout(%s/%s) newNetworkProvider failed: %s", c.Rollout.Namespace, c.Rollout.Name, err.Error())
klog.Errorf("%s newNetworkProvider failed: %s", c.Key, err.Error())
return false, err
}
cStep := c.Rollout.Spec.Strategy.Canary.Steps[canaryStatus.CurrentStepIndex-1]
steps := len(c.Rollout.Spec.Strategy.Canary.Steps)
cond := util.GetRolloutCondition(*c.NewStatus, v1alpha1.RolloutConditionProgressing)
cond.Message = fmt.Sprintf("Rollout is in step(%d/%d), and doing traffic routing", canaryStatus.CurrentStepIndex, steps)
verify, err := trController.EnsureRoutes(context.TODO(), cStep.Weight, cStep.Matches)
verify, err := trController.EnsureRoutes(context.TODO(), &c.Strategy)
if err != nil {
return false, err
} else if !verify {
klog.Infof("rollout(%s/%s) is doing step(%d) trafficRouting(%s)", c.Rollout.Namespace, c.Rollout.Name, canaryStatus.CurrentStepIndex, util.DumpJSON(cStep))
klog.Infof("%s is doing trafficRouting(%s), and wait a moment", c.Key, util.DumpJSON(c.Strategy))
return false, nil
}
klog.Infof("rollout(%s/%s) do step(%d) trafficRouting(%s) success", c.Rollout.Namespace, c.Rollout.Name, canaryStatus.CurrentStepIndex, util.DumpJSON(cStep))
klog.Infof("%s do trafficRouting(%s) success", c.Key, util.DumpJSON(c.Strategy))
return true, nil
}
func (m *Manager) FinalisingTrafficRouting(c *util.RolloutContext, onlyRestoreStableService bool) (bool, error) {
if len(c.Rollout.Spec.Strategy.Canary.TrafficRoutings) == 0 {
func (m *Manager) FinalisingTrafficRouting(c *TrafficRoutingContext, onlyRestoreStableService bool) (bool, error) {
if len(c.ObjectRef) == 0 {
return true, nil
}
trafficRouting := c.Rollout.Spec.Strategy.Canary.TrafficRoutings[0]
trafficRouting := c.ObjectRef[0]
if trafficRouting.GracePeriodSeconds <= 0 {
trafficRouting.GracePeriodSeconds = defaultGracePeriodSeconds
}
cServiceName := fmt.Sprintf("%s-canary", trafficRouting.Service)
trController, err := newNetworkProvider(m.Client, c.Rollout, c.NewStatus, trafficRouting.Service, cServiceName)
cServiceName := getCanaryServiceName(trafficRouting.Service, c.OnlyTrafficRouting)
trController, err := newNetworkProvider(m.Client, c, trafficRouting.Service, cServiceName)
if err != nil {
klog.Errorf("rollout(%s/%s) newTrafficRoutingController failed: %s", c.Rollout.Namespace, c.Rollout.Name, err.Error())
klog.Errorf("%s newTrafficRoutingController failed: %s", c.Key, err.Error())
return false, err
}
cService := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: c.Rollout.Namespace, Name: cServiceName}}
cService := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: c.Namespace, Name: cServiceName}}
// if canary svc has been already cleaned up, just return
if err = m.Get(context.TODO(), client.ObjectKeyFromObject(cService), cService); err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("rollout(%s/%s) get canary service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, cServiceName, err.Error())
klog.Errorf("%s get canary service(%s) failed: %s", c.Key, cServiceName, err.Error())
return false, err
}
// In rollout failure case, no canary-service will be created, this step ensures that the canary-ingress can be deleted in a time.
@ -197,10 +217,7 @@ func (m *Manager) FinalisingTrafficRouting(c *util.RolloutContext, onlyRestoreSt
return true, nil
}
if c.NewStatus.CanaryStatus == nil {
c.NewStatus.CanaryStatus = &v1alpha1.CanaryStatus{}
}
klog.Infof("rollout(%s/%s) start finalising traffic routing", c.Rollout.Namespace, c.Rollout.Name)
klog.Infof("%s start finalising traffic routing", c.Key)
// remove stable service the pod revision selector, so stable service will be selector all version pods.
verify, err := m.restoreStableService(c)
if err != nil || !verify {
@ -210,17 +227,18 @@ func (m *Manager) FinalisingTrafficRouting(c *util.RolloutContext, onlyRestoreSt
}
// First route 100% traffic to stable service
verify, err = trController.EnsureRoutes(context.TODO(), utilpointer.Int32(0), nil)
c.Strategy.Weight = utilpointer.Int32(0)
verify, err = trController.EnsureRoutes(context.TODO(), &c.Strategy)
if err != nil {
return false, err
} else if !verify {
c.NewStatus.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now()}
c.LastUpdateTime = &metav1.Time{Time: time.Now()}
return false, nil
}
if c.NewStatus.CanaryStatus.LastUpdateTime != nil {
if c.LastUpdateTime != nil {
// After restore the stable service configuration, give network provider 3 seconds to react
if verifyTime := c.NewStatus.CanaryStatus.LastUpdateTime.Add(time.Second * time.Duration(trafficRouting.GracePeriodSeconds)); verifyTime.After(time.Now()) {
klog.Infof("rollout(%s/%s) route 100% traffic to stable service, and wait a moment", c.Rollout.Namespace, c.Rollout.Name)
if verifyTime := c.LastUpdateTime.Add(time.Second * time.Duration(trafficRouting.GracePeriodSeconds)); verifyTime.After(time.Now()) {
klog.Infof("%s route 100% traffic to stable service, and wait a moment", c.Key)
return false, nil
}
}
@ -232,29 +250,29 @@ func (m *Manager) FinalisingTrafficRouting(c *util.RolloutContext, onlyRestoreSt
// remove canary service
err = m.Delete(context.TODO(), cService)
if err != nil && !errors.IsNotFound(err) {
klog.Errorf("rollout(%s/%s) remove canary service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, cService.Name, err.Error())
klog.Errorf("%s remove canary service(%s) failed: %s", c.Key, cService.Name, err.Error())
return false, err
}
klog.Infof("rollout(%s/%s) remove canary service(%s) success", c.Rollout.Namespace, c.Rollout.Name, cService.Name)
klog.Infof("%s remove canary service(%s) success", c.Key, cService.Name)
return true, nil
}
func newNetworkProvider(c client.Client, rollout *v1alpha1.Rollout, newStatus *v1alpha1.RolloutStatus, sService, cService string) (network.NetworkProvider, error) {
trafficRouting := rollout.Spec.Strategy.Canary.TrafficRoutings[0]
func newNetworkProvider(c client.Client, con *TrafficRoutingContext, sService, cService string) (network.NetworkProvider, error) {
trafficRouting := con.ObjectRef[0]
if trafficRouting.Ingress != nil {
return ingress.NewIngressTrafficRouting(c, ingress.Config{
RolloutName: rollout.Name,
RolloutNs: rollout.Namespace,
Key: con.Key,
Namespace: con.Namespace,
CanaryService: cService,
StableService: sService,
TrafficConf: trafficRouting.Ingress,
OwnerRef: *metav1.NewControllerRef(rollout, rolloutControllerKind),
OwnerRef: con.OwnerRef,
})
}
if trafficRouting.Gateway != nil {
return gateway.NewGatewayTrafficRouting(c, gateway.Config{
RolloutName: rollout.Name,
RolloutNs: rollout.Namespace,
Key: con.Key,
Namespace: con.Namespace,
CanaryService: cService,
StableService: sService,
TrafficConf: trafficRouting.Gateway,
@ -263,12 +281,12 @@ func newNetworkProvider(c client.Client, rollout *v1alpha1.Rollout, newStatus *v
return nil, fmt.Errorf("TrafficRouting current only support Ingress or Gateway API")
}
func (m *Manager) createCanaryService(c *util.RolloutContext, cService string, spec corev1.ServiceSpec) (*corev1.Service, error) {
func (m *Manager) createCanaryService(c *TrafficRoutingContext, cService string, spec corev1.ServiceSpec) (*corev1.Service, error) {
canaryService := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: c.Rollout.Namespace,
Namespace: c.Namespace,
Name: cService,
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(c.Rollout, rolloutControllerKind)},
OwnerReferences: []metav1.OwnerReference{c.OwnerRef},
},
Spec: spec,
}
@ -280,50 +298,57 @@ func (m *Manager) createCanaryService(c *util.RolloutContext, cService string, s
canaryService.Spec.IPFamilyPolicy = nil
canaryService.Spec.IPFamilies = nil
canaryService.Spec.LoadBalancerIP = ""
canaryService.Spec.Selector[c.Workload.RevisionLabelKey] = c.NewStatus.CanaryStatus.PodTemplateHash
canaryService.Spec.Selector[c.RevisionLabelKey] = c.CanaryRevision
err := m.Create(context.TODO(), canaryService)
if err != nil && !errors.IsAlreadyExists(err) {
klog.Errorf("rollout(%s/%s) create canary service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, cService, err.Error())
klog.Errorf("%s create canary service(%s) failed: %s", c.Key, cService, err.Error())
return nil, err
}
klog.Infof("rollout(%s/%s) create canary service(%s) success", c.Rollout.Namespace, c.Rollout.Name, util.DumpJSON(canaryService))
klog.Infof("%s create canary service(%s) success", c.Key, util.DumpJSON(canaryService))
return canaryService, nil
}
// remove stable service the pod revision selector, so stable service will be selector all version pods.
func (m *Manager) restoreStableService(c *util.RolloutContext) (bool, error) {
if c.Workload == nil {
return true, nil
func (m *Manager) restoreStableService(c *TrafficRoutingContext) (bool, error) {
trafficRouting := c.ObjectRef[0]
if trafficRouting.GracePeriodSeconds <= 0 {
trafficRouting.GracePeriodSeconds = defaultGracePeriodSeconds
}
trafficRouting := c.Rollout.Spec.Strategy.Canary.TrafficRoutings[0]
//fetch stable service
stableService := &corev1.Service{}
err := m.Get(context.TODO(), client.ObjectKey{Namespace: c.Rollout.Namespace, Name: trafficRouting.Service}, stableService)
err := m.Get(context.TODO(), client.ObjectKey{Namespace: c.Namespace, Name: trafficRouting.Service}, stableService)
if err != nil {
if errors.IsNotFound(err) {
return true, nil
}
klog.Errorf("rollout(%s/%s) get stable service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, trafficRouting.Service, err.Error())
klog.Errorf("%s get stable service(%s) failed: %s", c.Key, trafficRouting.Service, err.Error())
return false, err
}
if stableService.Spec.Selector[c.Workload.RevisionLabelKey] != "" {
body := fmt.Sprintf(`{"spec":{"selector":{"%s":null}}}`, c.Workload.RevisionLabelKey)
if stableService.Spec.Selector[c.RevisionLabelKey] != "" {
body := fmt.Sprintf(`{"spec":{"selector":{"%s":null}}}`, c.RevisionLabelKey)
if err = m.Patch(context.TODO(), stableService, client.RawPatch(types.StrategicMergePatchType, []byte(body))); err != nil {
klog.Errorf("rollout(%s/%s) patch stable service(%s) failed: %s", c.Rollout.Namespace, c.Rollout.Name, trafficRouting.Service, err.Error())
klog.Errorf("%s patch stable service(%s) failed: %s", c.Key, trafficRouting.Service, err.Error())
return false, err
}
klog.Infof("remove rollout(%s/%s) stable service(%s) pod revision selector, and wait a moment", c.Rollout.Namespace, c.Rollout.Name, trafficRouting.Service)
c.NewStatus.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now()}
klog.Infof("remove %s stable service(%s) pod revision selector, and wait a moment", c.Key, trafficRouting.Service)
c.LastUpdateTime = &metav1.Time{Time: time.Now()}
return false, nil
}
if c.NewStatus.CanaryStatus.LastUpdateTime == nil {
if c.LastUpdateTime == nil {
return true, nil
}
// After restore the stable service configuration, give network provider 3 seconds to react
if verifyTime := c.NewStatus.CanaryStatus.LastUpdateTime.Add(time.Second * time.Duration(trafficRouting.GracePeriodSeconds)); verifyTime.After(time.Now()) {
klog.Infof("rollout(%s/%s) restoring stable service(%s), and wait a moment", c.Rollout.Namespace, c.Rollout.Name, trafficRouting.Service)
if verifyTime := c.LastUpdateTime.Add(time.Second * time.Duration(trafficRouting.GracePeriodSeconds)); verifyTime.After(time.Now()) {
klog.Infof("%s restoring stable service(%s), and wait a moment", c.Key, trafficRouting.Service)
return false, nil
}
klog.Infof("rollout(%s/%s) doFinalising stable service(%s) success", c.Rollout.Namespace, c.Rollout.Name, trafficRouting.Service)
klog.Infof("%s doFinalising stable service(%s) success", c.Key, trafficRouting.Service)
return true, nil
}
func getCanaryServiceName(sService string, onlyTrafficRouting bool) string {
if onlyTrafficRouting {
return sService
}
return fmt.Sprintf("%s-canary", sService)
}

View File

@ -123,23 +123,31 @@ var (
Canary: &v1alpha1.CanaryStrategy{
Steps: []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(5),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(5),
},
Replicas: &intstr.IntOrString{IntVal: 1},
},
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Replicas: &intstr.IntOrString{IntVal: 2},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Replicas: &intstr.IntOrString{IntVal: 6},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Replicas: &intstr.IntOrString{IntVal: 10},
},
},
TrafficRoutings: []*v1alpha1.TrafficRouting{
TrafficRoutings: []v1alpha1.TrafficRoutingRef{
{
Service: "echoserver",
Ingress: &v1alpha1.IngressTrafficRouting{
@ -283,7 +291,13 @@ func TestDoTrafficRouting(t *testing.T) {
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{demoIngress.DeepCopy()}
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "0"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1alpha1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
@ -385,6 +399,10 @@ func TestDoTrafficRouting(t *testing.T) {
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
if cs.name != "DoTrafficRouting test3" {
return
}
fmt.Println("start DoTrafficRouting test3")
ss, ig := cs.getObj()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ig[0], ss[0], demoConf.DeepCopy()).Build()
if len(ss) == 2 {
@ -393,9 +411,20 @@ func TestDoTrafficRouting(t *testing.T) {
if len(ig) == 2 {
_ = client.Create(context.TODO(), ig[1])
}
c := &util.RolloutContext{}
c.Rollout, c.Workload = cs.getRollout()
c.NewStatus = c.Rollout.Status.DeepCopy()
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1alpha1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
}
manager := NewTrafficRoutingManager(client)
err := manager.InitializeTrafficRouting(c)
if err != nil {
@ -621,9 +650,20 @@ func TestFinalisingTrafficRouting(t *testing.T) {
if len(ig) == 2 {
_ = client.Create(context.TODO(), ig[1])
}
c := &util.RolloutContext{}
c.Rollout, c.Workload = cs.getRollout()
c.NewStatus = c.Rollout.Status.DeepCopy()
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1alpha1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
}
manager := NewTrafficRoutingManager(client)
done, err := manager.FinalisingTrafficRouting(c, cs.onlyRestoreStableService)
if err != nil {

View File

@ -29,8 +29,9 @@ import (
)
type Config struct {
RolloutName string
RolloutNs string
// only for log info
Key string
Namespace string
CanaryService string
StableService string
TrafficConf *rolloutv1alpha1.GatewayTrafficRouting
@ -52,17 +53,20 @@ func NewGatewayTrafficRouting(client client.Client, conf Config) (network.Networ
func (r *gatewayController) Initialize(ctx context.Context) error {
route := &gatewayv1alpha2.HTTPRoute{}
return r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: *r.conf.TrafficConf.HTTPRouteName}, route)
return r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: *r.conf.TrafficConf.HTTPRouteName}, route)
}
func (r *gatewayController) EnsureRoutes(ctx context.Context, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (bool, error) {
func (r *gatewayController) EnsureRoutes(ctx context.Context, strategy *rolloutv1alpha1.TrafficRoutingStrategy) (bool, error) {
weight := strategy.Weight
matches := strategy.Matches
// headerModifier := strategy.RequestHeaderModifier
var httpRoute gatewayv1alpha2.HTTPRoute
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: *r.conf.TrafficConf.HTTPRouteName}, &httpRoute)
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: *r.conf.TrafficConf.HTTPRouteName}, &httpRoute)
if err != nil {
return false, err
}
// desired route
desiredRule := r.buildDesiredHTTPRoute(httpRoute.Spec.Rules, weight, matches)
desiredRule := r.buildDesiredHTTPRoute(httpRoute.Spec.Rules, weight, matches, nil)
if reflect.DeepEqual(httpRoute.Spec.Rules, desiredRule) {
return true, nil
}
@ -76,25 +80,25 @@ func (r *gatewayController) EnsureRoutes(ctx context.Context, weight *int32, mat
routeClone.Spec.Rules = desiredRule
return r.Client.Update(context.TODO(), routeClone)
}); err != nil {
klog.Errorf("update rollout(%s/%s) httpRoute(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, httpRoute.Name, err.Error())
klog.Errorf("update %s httpRoute(%s) failed: %s", r.conf.Key, httpRoute.Name, err.Error())
return false, err
}
klog.Infof("rollout(%s/%s) set HTTPRoute(name:%s weight:%d) success", r.conf.RolloutNs, r.conf.RolloutName, *r.conf.TrafficConf.HTTPRouteName, *weight)
klog.Infof("%s set HTTPRoute(name:%s weight:%d) success", r.conf.Key, *r.conf.TrafficConf.HTTPRouteName, *weight)
return false, nil
}
func (r *gatewayController) Finalise(ctx context.Context) error {
httpRoute := &gatewayv1alpha2.HTTPRoute{}
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.RolloutNs, Name: *r.conf.TrafficConf.HTTPRouteName}, httpRoute)
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: *r.conf.TrafficConf.HTTPRouteName}, httpRoute)
if err != nil {
if errors.IsNotFound(err) {
return nil
}
klog.Errorf("rollout(%s/%s) get HTTPRoute failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
klog.Errorf("%s get HTTPRoute failed: %s", r.conf.Key, err.Error())
return err
}
// desired rule
desiredRule := r.buildDesiredHTTPRoute(httpRoute.Spec.Rules, utilpointer.Int32(-1), nil)
desiredRule := r.buildDesiredHTTPRoute(httpRoute.Spec.Rules, utilpointer.Int32(-1), nil, nil)
if reflect.DeepEqual(httpRoute.Spec.Rules, desiredRule) {
return nil
}
@ -107,14 +111,15 @@ func (r *gatewayController) Finalise(ctx context.Context) error {
routeClone.Spec.Rules = desiredRule
return r.Client.Update(context.TODO(), routeClone)
}); err != nil {
klog.Errorf("update rollout(%s/%s) httpRoute(%s) failed: %s", r.conf.RolloutNs, r.conf.RolloutName, httpRoute.Name, err.Error())
klog.Errorf("update %s httpRoute(%s) failed: %s", r.conf.Key, httpRoute.Name, err.Error())
return err
}
klog.Infof("rollout(%s/%s) TrafficRouting Finalise success", r.conf.RolloutNs, r.conf.RolloutName)
klog.Infof("%s TrafficRouting Finalise success", r.conf.Key)
return nil
}
func (r *gatewayController) buildDesiredHTTPRoute(rules []gatewayv1alpha2.HTTPRouteRule, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) []gatewayv1alpha2.HTTPRouteRule {
func (r *gatewayController) buildDesiredHTTPRoute(rules []gatewayv1alpha2.HTTPRouteRule, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch,
rh *gatewayv1alpha2.HTTPRequestHeaderFilter) []gatewayv1alpha2.HTTPRouteRule {
var desired []gatewayv1alpha2.HTTPRouteRule
// Only when finalize method parameter weight=-1,
// then we need to remove the canary route policy and restore to the original configuration

View File

@ -443,7 +443,7 @@ func TestBuildDesiredHTTPRoute(t *testing.T) {
}
conf := Config{
RolloutName: "rollout-demo",
Key: "rollout-demo",
CanaryService: "store-svc-canary",
StableService: "store-svc",
}
@ -452,7 +452,7 @@ func TestBuildDesiredHTTPRoute(t *testing.T) {
t.Run(cs.name, func(t *testing.T) {
controller := &gatewayController{conf: conf}
weight, matches := cs.getRoutes()
current := controller.buildDesiredHTTPRoute(cs.getRouteRules(), weight, matches)
current := controller.buildDesiredHTTPRoute(cs.getRouteRules(), weight, matches, nil)
desired := cs.desiredRules()
if !reflect.DeepEqual(current, desired) {
t.Fatalf("expect: %v, but get %v", util.DumpJSON(desired), util.DumpJSON(current))

View File

@ -50,8 +50,9 @@ type ingressController struct {
}
type Config struct {
RolloutName string
RolloutNs string
// only for log info
Key string
Namespace string
CanaryService string
StableService string
TrafficConf *rolloutv1alpha1.IngressTrafficRouting
@ -73,50 +74,51 @@ func NewIngressTrafficRouting(client client.Client, conf Config) (network.Networ
return r, nil
}
// Initialize verify the existence of the ingress resource and generate the canary ingress
// Initialize only determine if the network resources(ingress & gateway api) exist.
// If error is nil, then the network resources exist.
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
return r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: r.conf.TrafficConf.Name}, ingress)
}
func (r *ingressController) EnsureRoutes(ctx context.Context, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (bool, error) {
func (r *ingressController) EnsureRoutes(ctx context.Context, strategy *rolloutv1alpha1.TrafficRoutingStrategy) (bool, error) {
weight := strategy.Weight
matches := strategy.Matches
// headerModifier := strategy.RequestHeaderModifier
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) {
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: defaultCanaryIngressName(r.conf.TrafficConf.Name)}, canaryIngress)
if errors.IsNotFound(err) {
// finalizer scenario, canary ingress maybe not found
if weight != nil && *weight == 0 {
return true, nil
}
klog.Errorf("rollout(%s/%s) get canary ingress failed: %s", r.conf.RolloutNs, r.conf.RolloutName, err.Error())
// create canary ingress
ingress := &netv1.Ingress{}
err = r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: r.conf.TrafficConf.Name}, ingress)
if err != nil {
return false, err
}
// 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("%s execute lua failed: %s", r.conf.Key, err.Error())
return false, err
}
if err = r.Create(ctx, canaryIngress); err != nil {
klog.Errorf("%s create canary ingress failed: %s", r.conf.Key, err.Error())
return false, err
}
klog.Infof("%s create canary ingress(%s) success", r.conf.Key, util.DumpJSON(canaryIngress))
return false, nil
} else if err != nil {
klog.Errorf("%s get canary ingress failed: %s", r.conf.Key, 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())
klog.Errorf("%s execute lua failed: %s", r.conf.Key, err.Error())
return false, err
}
if reflect.DeepEqual(canaryIngress.Annotations, newAnnotations) {
@ -126,23 +128,23 @@ func (r *ingressController) EnsureRoutes(ctx context.Context, weight *int32, mat
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())
klog.Errorf("%s create merge patch failed: %s", r.conf.Key, 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())
klog.Errorf("%s set canary ingress(%s) failed: %s", r.conf.Key, 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))
klog.Infof("%s set canary ingress(%s) annotations(%s) success", r.conf.Key, 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)
err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, 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())
klog.Errorf("%s get canary ingress(%s) failed: %s", r.conf.Key, r.canaryIngressName, err.Error())
return err
}
if errors.IsNotFound(err) || !canaryIngress.DeletionTimestamp.IsZero() {
@ -150,10 +152,10 @@ func (r *ingressController) Finalise(ctx context.Context) error {
}
// 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())
klog.Errorf("%s remove canary ingress(%s) failed: %s", r.conf.Key, canaryIngress.Name, err.Error())
return err
}
klog.Infof("rollout(%s/%s) remove canary ingress(%s) success", r.conf.RolloutNs, r.conf.RolloutName, canaryIngress.Name)
klog.Infof("%s remove canary ingress(%s) success", r.conf.Key, canaryIngress.Name)
return nil
}
@ -215,6 +217,7 @@ func defaultCanaryIngressName(name string) string {
}
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.
@ -225,12 +228,14 @@ func (r *ingressController) executeLuaForCanary(annotations map[string]string, w
Weight string
Matches []rolloutv1alpha1.HttpRouteMatch
CanaryService string
//todo, RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter
}
data := &LuaData{
Annotations: annotations,
Weight: fmt.Sprintf("%d", *weight),
Matches: matches,
CanaryService: r.conf.CanaryService,
// RequestHeaderModifier: headerModifier,
}
unObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(data)
if err != nil {

View File

@ -44,6 +44,12 @@ var (
},
Data: map[string]string{
fmt.Sprintf("%s.nginx", 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["nginx.ingress.kubernetes.io/canary"] = "true"
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = nil
@ -55,6 +61,14 @@ var (
then
annotations["nginx.ingress.kubernetes.io/canary-weight"] = obj.weight
end
if ( obj.requestHeaderModifier )
then
local str = ''
for _,header in ipairs(obj.requestHeaderModifier.set) do
str = str..string.format("%s %s\n", header.name, header.value)
end
annotations["mse.ingress.kubernetes.io/request-header-control-update"] = str
end
if ( not obj.matches )
then
return annotations
@ -240,10 +254,9 @@ func init() {
func TestInitialize(t *testing.T) {
cases := []struct {
name string
getConfigmap func() *corev1.ConfigMap
getIngress func() []*netv1.Ingress
expectIngress func() *netv1.Ingress
name string
getConfigmap func() *corev1.ConfigMap
getIngress func() []*netv1.Ingress
}{
{
name: "init test1",
@ -253,21 +266,11 @@ func TestInitialize(t *testing.T) {
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",
Key: "rollout-demo",
StableService: "echoserver",
CanaryService: "echoserver-canary",
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
@ -291,17 +294,6 @@ func TestInitialize(t *testing.T) {
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))
}
})
}
}
@ -311,7 +303,7 @@ func TestEnsureRoutes(t *testing.T) {
name string
getConfigmap func() *corev1.ConfigMap
getIngress func() []*netv1.Ingress
getRoutes func() (*int32, []rolloutsv1alpha1.HttpRouteMatch)
getRoutes func() *rolloutsv1alpha1.CanaryStep
expectIngress func() *netv1.Ingress
ingressType string
}{
@ -330,25 +322,42 @@ func TestEnsureRoutes(t *testing.T) {
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{
getRoutes: func() *rolloutsv1alpha1.CanaryStep {
return &rolloutsv1alpha1.CanaryStep{
TrafficRoutingStrategy: rolloutsv1alpha1.TrafficRoutingStrategy{
Weight: nil,
Matches: []rolloutsv1alpha1.HttpRouteMatch{
// header
{
Name: "user_id",
Value: "123456",
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "user_id",
Value: "123456",
},
},
},
// cookies
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "canary-by-cookie",
Value: "demo",
},
},
},
},
},
// cookies
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "canary-by-cookie",
Value: "demo",
/*RequestHeaderModifier: &gatewayv1alpha2.HTTPRequestHeaderFilter{
Set: []gatewayv1alpha2.HTTPHeader{
{
Name: "gray",
Value: "blue",
},
{
Name: "gray",
Value: "green",
},
},
},
},*/
},
}
},
@ -359,6 +368,7 @@ func TestEnsureRoutes(t *testing.T) {
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.Annotations["mse.ingress.kubernetes.io/request-header-control-update"] = "gray blue\ngray green\n"
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"
@ -382,8 +392,12 @@ func TestEnsureRoutes(t *testing.T) {
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
getRoutes: func() *rolloutsv1alpha1.CanaryStep {
return &rolloutsv1alpha1.CanaryStep{
TrafficRoutingStrategy: rolloutsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
}
},
expectIngress: func() *netv1.Ingress {
expect := demoIngress.DeepCopy()
@ -413,16 +427,20 @@ func TestEnsureRoutes(t *testing.T) {
canary.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*netv1.Ingress{demoIngress.DeepCopy(), canary}
},
getRoutes: func() (*int32, []rolloutsv1alpha1.HttpRouteMatch) {
getRoutes: func() *rolloutsv1alpha1.CanaryStep {
iType := gatewayv1alpha2.HeaderMatchRegularExpression
return nil, []rolloutsv1alpha1.HttpRouteMatch{
// header
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
return &rolloutsv1alpha1.CanaryStep{
TrafficRoutingStrategy: rolloutsv1alpha1.TrafficRoutingStrategy{
Matches: []rolloutsv1alpha1.HttpRouteMatch{
// header
{
Name: "user_id",
Value: "123*",
Type: &iType,
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "user_id",
Value: "123*",
Type: &iType,
},
},
},
},
},
@ -457,22 +475,26 @@ func TestEnsureRoutes(t *testing.T) {
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{
getRoutes: func() *rolloutsv1alpha1.CanaryStep {
return &rolloutsv1alpha1.CanaryStep{
TrafficRoutingStrategy: rolloutsv1alpha1.TrafficRoutingStrategy{
Matches: []rolloutsv1alpha1.HttpRouteMatch{
// header
{
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",
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",
},
},
},
},
},
@ -493,7 +515,7 @@ func TestEnsureRoutes(t *testing.T) {
}
config := Config{
RolloutName: "rollout-demo",
Key: "rollout-demo",
StableService: "echoserver",
CanaryService: "echoserver-canary",
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{
@ -513,8 +535,8 @@ func TestEnsureRoutes(t *testing.T) {
t.Fatalf("NewIngressTrafficRouting failed: %s", err.Error())
return
}
weight, matches := cs.getRoutes()
_, err = controller.EnsureRoutes(context.TODO(), weight, matches)
step := cs.getRoutes()
_, err = controller.EnsureRoutes(context.TODO(), &step.TrafficRoutingStrategy)
if err != nil {
t.Fatalf("EnsureRoutes failed: %s", err.Error())
return
@ -528,7 +550,7 @@ func TestEnsureRoutes(t *testing.T) {
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))
t.Fatalf("but get(%s)", util.DumpJSON(canaryIngress))
}
})
}
@ -563,7 +585,7 @@ func TestFinalise(t *testing.T) {
}
config := Config{
RolloutName: "rollout-demo",
Key: "rollout-demo",
StableService: "echoserver",
CanaryService: "echoserver-canary",
TrafficConf: &rolloutsv1alpha1.IngressTrafficRouting{

View File

@ -24,8 +24,8 @@ import (
// NetworkProvider common function across all TrafficRouting implementation
type NetworkProvider interface {
// Initialize determine if the network resources(ingress & gateway api) exist.
// If it is Ingress, init method will create the canary ingress resources, and set weight=0.
// Initialize only determine if the network resources(ingress & gateway api) exist.
// If error is nil, then the network resources exist.
Initialize(ctx context.Context) error
// EnsureRoutes check and set canary weight and matches.
// weight indicates percentage of traffic to canary service, and range of values[0,100]
@ -33,7 +33,7 @@ type NetworkProvider interface {
// 1. check if canary has been set desired weight.
// 2. If not, set canary desired weight
// When the first set weight is returned false, mainly to give the provider some time to process, only when again ensure, will return true
EnsureRoutes(ctx context.Context, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (bool, error)
EnsureRoutes(ctx context.Context, strategy *rolloutv1alpha1.TrafficRoutingStrategy) (bool, error)
// Finalise will do some cleanup work after the canary rollout complete, such as delete canary ingress.
// Finalise is called with a 3-second delay after completing the canary.
Finalise(ctx context.Context) error

View File

@ -16,6 +16,12 @@ limitations under the License.
package util
import (
"fmt"
"github.com/openkruise/rollouts/api/v1alpha1"
)
// For Rollout and BatchRelease
const (
// BatchReleaseControlAnnotation is controller info about batchRelease when rollout
@ -41,6 +47,10 @@ const (
DeploymentRevisionAnnotation = "deployment.kubernetes.io/revision"
)
const (
TrafficRoutingFinalizer = "rollouts.kruise.io/trafficrouting"
)
// For Pods
const (
// NoNeedUpdatePodLabel will be patched to pod when rollback in batches if the pods no need to rollback
@ -57,6 +67,7 @@ const (
CloneSetType WorkloadType = "cloneset"
DeploymentType WorkloadType = "deployment"
StatefulSetType WorkloadType = "statefulset"
DaemonSetType WorkloadType = "daemonset"
AddFinalizerOpType FinalizerOpType = "Add"
RemoveFinalizerOpType FinalizerOpType = "Remove"
@ -65,3 +76,7 @@ const (
type WorkloadType string
type FinalizerOpType string
func ProgressingRolloutFinalizer(name string) string {
return fmt.Sprintf("%s/%s", v1alpha1.ProgressingRolloutFinalizerPrefix, name)
}

View File

@ -1,34 +0,0 @@
/*
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 util
import (
"time"
"github.com/openkruise/rollouts/api/v1alpha1"
)
type RolloutContext struct {
Rollout *v1alpha1.Rollout
NewStatus *v1alpha1.RolloutStatus
// related workload
Workload *Workload
// reconcile RequeueAfter recheckTime
RecheckTime *time.Time
// wait stable workload pods ready
WaitReady bool
}

View File

@ -37,6 +37,7 @@ import (
// Workload is used to return (controller, scale, selector) fields from the
// controller finder functions.
type Workload struct {
metav1.TypeMeta
metav1.ObjectMeta
// replicas
@ -49,6 +50,8 @@ type Workload struct {
PodTemplateHash string
// Revision hash key
RevisionLabelKey string
// label selector
Selector *metav1.LabelSelector
// Is it in rollback phase
IsInRollback bool
@ -159,6 +162,7 @@ func (r *ControllerFinder) getKruiseCloneSet(namespace string, ref *rolloutv1alp
StableRevision: cloneSet.Status.CurrentRevision[strings.LastIndex(cloneSet.Status.CurrentRevision, "-")+1:],
CanaryRevision: cloneSet.Status.UpdateRevision[strings.LastIndex(cloneSet.Status.UpdateRevision, "-")+1:],
ObjectMeta: cloneSet.ObjectMeta,
TypeMeta: cloneSet.TypeMeta,
Replicas: *cloneSet.Spec.Replicas,
PodTemplateHash: cloneSet.Status.UpdateRevision[strings.LastIndex(cloneSet.Status.UpdateRevision, "-")+1:],
IsStatusConsistent: true,
@ -200,6 +204,7 @@ func (r *ControllerFinder) getKruiseDaemonSet(namespace string, ref *rolloutv1al
//StableRevision: daemonSet.Status.CurrentRevision[strings.LastIndex(cloneSet.Status.CurrentRevision, "-")+1:],
CanaryRevision: daemonSet.Status.DaemonSetHash[strings.LastIndex(daemonSet.Status.DaemonSetHash, "-")+1:],
ObjectMeta: daemonSet.ObjectMeta,
TypeMeta: daemonSet.TypeMeta,
Replicas: daemonSet.Status.DesiredNumberScheduled,
PodTemplateHash: daemonSet.Status.DaemonSetHash[strings.LastIndex(daemonSet.Status.DaemonSetHash, "-")+1:],
IsStatusConsistent: true,
@ -246,6 +251,7 @@ func (r *ControllerFinder) getAdvancedDeployment(namespace string, ref *rolloutv
StableRevision: stableRevision,
CanaryRevision: ComputeHash(&deployment.Spec.Template, nil),
ObjectMeta: deployment.ObjectMeta,
TypeMeta: deployment.TypeMeta,
Replicas: *deployment.Spec.Replicas,
IsStatusConsistent: true,
}
@ -299,11 +305,13 @@ func (r *ControllerFinder) getDeployment(namespace string, ref *rolloutv1alpha1.
workload := &Workload{
ObjectMeta: stable.ObjectMeta,
TypeMeta: stable.TypeMeta,
Replicas: *stable.Spec.Replicas,
IsStatusConsistent: true,
StableRevision: stableRs.Labels[apps.DefaultDeploymentUniqueLabelKey],
CanaryRevision: ComputeHash(&stable.Spec.Template, nil),
RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey,
Selector: stable.Spec.Selector,
}
// not in rollout progressing
@ -357,6 +365,7 @@ func (r *ControllerFinder) getStatefulSetLikeWorkload(namespace string, ref *rol
StableRevision: workloadInfo.Status.StableRevision,
CanaryRevision: workloadInfo.Status.UpdateRevision,
ObjectMeta: workloadInfo.ObjectMeta,
TypeMeta: workloadInfo.TypeMeta,
Replicas: workloadInfo.Replicas,
PodTemplateHash: workloadInfo.Status.UpdateRevision,
IsStatusConsistent: true,

View File

@ -32,7 +32,7 @@ func init() {
luaConfigurationList = map[string]string{}
_ = filepath.Walk("./lua_configuration", func(path string, f os.FileInfo, err error) error {
if err != nil {
klog.Errorf("filepath walk ./lua_configuration failed: %s", err.Error())
klog.Warningf("filepath walk ./lua_configuration failed: %s", err.Error())
return err
}
if f.IsDir() {

View File

@ -42,6 +42,7 @@ func ParseWorkload(object client.Object) *WorkloadInfo {
return &WorkloadInfo{
LogKey: fmt.Sprintf("%s (%s)", key, gvk),
ObjectMeta: *GetMetadata(object),
TypeMeta: *GetTypeMeta(object),
Replicas: GetReplicas(object),
Status: *ParseWorkloadStatus(object),
}
@ -332,6 +333,27 @@ func GetMetadata(object client.Object) *metav1.ObjectMeta {
}
}
// GetTypeMeta can parse the whole TypeMeta field from client workload object
func GetTypeMeta(object client.Object) *metav1.TypeMeta {
switch o := object.(type) {
case *apps.Deployment:
return &o.TypeMeta
case *appsv1alpha1.CloneSet:
return &o.TypeMeta
case *apps.StatefulSet:
return &o.TypeMeta
case *appsv1beta1.StatefulSet:
return &o.TypeMeta
case *appsv1alpha1.DaemonSet:
return &o.TypeMeta
case *unstructured.Unstructured:
gvk := object.GetObjectKind().GroupVersionKind()
return &metav1.TypeMeta{APIVersion: gvk.GroupVersion().String(), Kind: gvk.Kind}
default:
panic("unsupported workload type to ParseSelector function")
}
}
// parseReplicasFromUnstructured parses replicas from unstructured workload object
func parseReplicasFromUnstructured(object *unstructured.Unstructured) int32 {
replicas := int32(1)

View File

@ -71,6 +71,7 @@ type WorkloadStatus struct {
}
type WorkloadInfo struct {
metav1.TypeMeta
metav1.ObjectMeta
LogKey string
Replicas int32

View File

@ -67,11 +67,15 @@ func (h *RolloutCreateUpdateHandler) Handle(ctx context.Context, req admission.R
if err := h.Decoder.Decode(req, obj); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
errList := h.validateRollout(obj)
if len(errList) != 0 {
return admission.Errored(http.StatusUnprocessableEntity, errList.ToAggregate())
}
oldObj := &appsv1alpha1.Rollout{}
if err := h.Decoder.DecodeRaw(req.AdmissionRequest.OldObject, oldObj); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
errList := h.validateRolloutUpdate(oldObj, obj)
errList = h.validateRolloutUpdate(oldObj, obj)
if len(errList) != 0 {
return admission.Errored(http.StatusUnprocessableEntity, errList.ToAggregate())
}
@ -199,11 +203,7 @@ func validateRolloutSpecCanaryStrategy(canary *appsv1alpha1.CanaryStrategy, fldP
return errList
}
func validateRolloutSpecCanaryTraffic(traffic *appsv1alpha1.TrafficRouting, fldPath *field.Path) field.ErrorList {
if traffic == nil {
return field.ErrorList{field.Invalid(fldPath, nil, "Canary.TrafficRoutings cannot be empty")}
}
func validateRolloutSpecCanaryTraffic(traffic appsv1alpha1.TrafficRoutingRef, fldPath *field.Path) field.ErrorList {
errList := field.ErrorList{}
if len(traffic.Service) == 0 {
errList = append(errList, field.Invalid(fldPath.Child("Service"), traffic.Service, "TrafficRouting.Service cannot be empty"))

View File

@ -56,30 +56,42 @@ var (
Canary: &appsv1alpha1.CanaryStrategy{
Steps: []appsv1alpha1.CanaryStep{
{
Weight: utilpointer.Int32Ptr(10),
TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(10),
},
Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(1)},
Pause: appsv1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32Ptr(10),
TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(10),
},
Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(3)},
Pause: appsv1alpha1.RolloutPause{Duration: utilpointer.Int32(1 * 24 * 60 * 60)},
},
{
Weight: utilpointer.Int32Ptr(30),
Pause: appsv1alpha1.RolloutPause{Duration: utilpointer.Int32(7 * 24 * 60 * 60)},
TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(30),
},
Pause: appsv1alpha1.RolloutPause{Duration: utilpointer.Int32(7 * 24 * 60 * 60)},
},
{
Weight: utilpointer.Int32Ptr(100),
TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
},
{
Weight: utilpointer.Int32Ptr(101),
TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(101),
},
},
{
Weight: utilpointer.Int32Ptr(200),
TrafficRoutingStrategy: appsv1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(200),
},
},
},
TrafficRoutings: []*appsv1alpha1.TrafficRouting{
TrafficRoutings: []appsv1alpha1.TrafficRoutingRef{
{
Service: "service-demo",
Ingress: &appsv1alpha1.IngressTrafficRouting{

View File

@ -376,7 +376,7 @@ func TestHandlerDeployment(t *testing.T) {
getRollout: func() *appsv1alpha1.Rollout {
demo := rolloutDemo.DeepCopy()
demo.Spec.Strategy.Canary = &appsv1alpha1.CanaryStrategy{
TrafficRoutings: []*appsv1alpha1.TrafficRouting{
TrafficRoutings: []appsv1alpha1.TrafficRoutingRef{
{
Service: "echoserver",
Ingress: &appsv1alpha1.IngressTrafficRouting{
@ -550,8 +550,7 @@ func TestHandlerDeployment(t *testing.T) {
}
delete(newObj.Labels, appsv1alpha1.DeploymentStableRevisionLabel)
if !reflect.DeepEqual(newObj, cs.expectObj()) {
by, _ := json.Marshal(newObj)
t.Fatalf("handlerDeployment failed, and new(%s)", string(by))
t.Fatalf("handlerDeployment failed, and expect(%s) new(%s)", util.DumpJSON(cs.expectObj()), util.DumpJSON(newObj))
}
})
}

View File

@ -245,6 +245,8 @@ var _ = SIGDescribe("Rollout", func() {
Eventually(func() bool {
daemon := &appsv1alpha1.DaemonSet{}
Expect(GetObject(daemonset.Name, daemon)).NotTo(HaveOccurred())
klog.Infof("DaemonSet Generation(%d) ObservedGeneration(%d) DesiredNumberScheduled(%d) UpdatedNumberScheduled(%d) NumberReady(%d)",
daemon.Generation, daemon.Status.ObservedGeneration, daemon.Status.DesiredNumberScheduled, daemon.Status.UpdatedNumberScheduled, daemon.Status.NumberReady)
return daemon.Status.ObservedGeneration == daemon.Generation && daemon.Status.DesiredNumberScheduled == daemon.Status.UpdatedNumberScheduled && daemon.Status.DesiredNumberScheduled == daemon.Status.NumberReady
}, 5*time.Minute, time.Second).Should(BeTrue())
}
@ -415,19 +417,29 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
},
{
Weight: utilpointer.Int32(40),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
},
{
Weight: utilpointer.Int32(80),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(80),
},
},
{
Weight: utilpointer.Int32(90),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(90),
},
},
}
CreateObject(rollout)
@ -561,13 +573,17 @@ var _ = SIGDescribe("Rollout", func() {
replicas := intstr.FromInt(2)
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Replicas: &replicas,
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
}
CreateObject(rollout)
@ -996,23 +1012,31 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(40),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
@ -1129,23 +1153,31 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(40),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
@ -1258,31 +1290,41 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
},
{
Weight: utilpointer.Int32(40),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
},
{
Weight: utilpointer.Int32(80),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(80),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
@ -1389,19 +1431,25 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
@ -1450,7 +1498,6 @@ var _ = SIGDescribe("Rollout", func() {
Expect(k8sClient.DeleteAllOf(context.TODO(), &v1alpha1.Rollout{}, client.InNamespace(namespace), client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
WaitRolloutNotFound(rollout.Name)
Expect(GetObject(workload.Name, workload)).NotTo(HaveOccurred())
fmt.Println(util.DumpJSON(workload))
workload.Spec.Paused = false
UpdateDeployment(workload)
By("Update deployment paused=false")
@ -1485,19 +1532,25 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
@ -1631,15 +1684,21 @@ var _ = SIGDescribe("Rollout", func() {
// update rollout step configuration
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(10),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(10),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(30),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(30),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(5),
},
@ -1732,21 +1791,23 @@ var _ = SIGDescribe("Rollout", func() {
replica2 := intstr.FromInt(2)
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Matches: []v1alpha1.HttpRouteMatch{
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Type: &headerType,
Name: "user_id",
Value: "123456",
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Matches: []v1alpha1.HttpRouteMatch{
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Type: &headerType,
Name: "user_id",
Value: "123456",
},
},
},
},
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "canary-by-cookie",
Value: "demo",
{
Headers: []gatewayv1alpha2.HTTPHeaderMatch{
{
Name: "canary-by-cookie",
Value: "demo",
},
},
},
},
@ -1755,7 +1816,9 @@ var _ = SIGDescribe("Rollout", func() {
Replicas: &replica1,
},
{
Weight: utilpointer.Int32(30),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(30),
},
Replicas: &replica2,
Pause: v1alpha1.RolloutPause{},
},
@ -1894,20 +1957,22 @@ var _ = SIGDescribe("Rollout", func() {
replica2 := intstr.FromInt(2)
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Matches: []v1alpha1.HttpRouteMatch{
{
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",
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Matches: []v1alpha1.HttpRouteMatch{
{
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",
},
},
},
},
@ -1916,7 +1981,9 @@ var _ = SIGDescribe("Rollout", func() {
Replicas: &replica1,
},
{
Weight: utilpointer.Int32(30),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(30),
},
Replicas: &replica2,
Pause: v1alpha1.RolloutPause{},
},
@ -2062,20 +2129,22 @@ var _ = SIGDescribe("Rollout", func() {
}
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Matches: []v1alpha1.HttpRouteMatch{
{
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",
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Matches: []v1alpha1.HttpRouteMatch{
{
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",
},
},
},
},
@ -2220,19 +2289,29 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/gateway/rollout-test.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
},
{
Weight: utilpointer.Int32(40),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
},
{
Weight: utilpointer.Int32(80),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(80),
},
},
{
Weight: utilpointer.Int32(90),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(90),
},
},
}
CreateObject(rollout)
@ -2378,12 +2457,16 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -2873,7 +2956,7 @@ var _ = SIGDescribe("Rollout", func() {
WaitRolloutWorkloadGeneration(rollout.Name, workload.Generation)
})
It("V1->V2: Percentage, 20%,40%,60%,80%,100%, no traffic, Succeeded", func() {
It("CloneSet V1->V2: Percentage, 20%,40%,60%,80%,100%, no traffic, Succeeded", func() {
By("Creating Rollout...")
rollout := &v1alpha1.Rollout{}
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
@ -2987,12 +3070,16 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -3493,12 +3580,16 @@ var _ = SIGDescribe("Rollout", func() {
Expect(ReadYamlToObject("./test_data/rollout/rollout_canary_base.yaml", rollout)).ToNot(HaveOccurred())
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -4069,23 +4160,31 @@ var _ = SIGDescribe("Rollout", func() {
}
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(40),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(10),
},
@ -4165,23 +4264,33 @@ var _ = SIGDescribe("Rollout", func() {
}
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(40),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(80),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(80),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
@ -4271,23 +4380,33 @@ var _ = SIGDescribe("Rollout", func() {
}
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(40),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(80),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(80),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
@ -4361,23 +4480,33 @@ var _ = SIGDescribe("Rollout", func() {
}
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(40),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(40),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(80),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(80),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{
Duration: utilpointer.Int32(0),
},
@ -4451,20 +4580,28 @@ var _ = SIGDescribe("Rollout", func() {
FailureThreshold: &intstr.IntOrString{Type: intstr.String, StrVal: "20%"},
Steps: []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(10),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(10),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(30),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(30),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{},
},
},
}
@ -4524,12 +4661,16 @@ var _ = SIGDescribe("Rollout", func() {
}
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -4682,16 +4823,22 @@ var _ = SIGDescribe("Rollout", func() {
rollout.Spec.Strategy.Canary.TrafficRoutings = nil
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -4788,16 +4935,22 @@ var _ = SIGDescribe("Rollout", func() {
rollout.Spec.Strategy.Canary.TrafficRoutings = nil
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -4871,16 +5024,22 @@ var _ = SIGDescribe("Rollout", func() {
rollout.Spec.Strategy.Canary.TrafficRoutings = nil
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -4942,16 +5101,22 @@ var _ = SIGDescribe("Rollout", func() {
rollout.Spec.Strategy.Canary.TrafficRoutings = nil
rollout.Spec.Strategy.Canary.Steps = []v1alpha1.CanaryStep{
{
Weight: utilpointer.Int32(20),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(20),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(60),
Pause: v1alpha1.RolloutPause{},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(60),
},
Pause: v1alpha1.RolloutPause{},
},
{
Weight: utilpointer.Int32(100),
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
TrafficRoutingStrategy: v1alpha1.TrafficRoutingStrategy{
Weight: utilpointer.Int32(100),
},
Pause: v1alpha1.RolloutPause{Duration: utilpointer.Int32(0)},
},
}
rollout.Spec.ObjectRef.WorkloadRef = &v1alpha1.WorkloadRef{
@ -5285,7 +5450,7 @@ var _ = SIGDescribe("Rollout", func() {
By("check rollout status & paused success")
// v1 -> v2, start rollout action
By("Update deployment env NODE_NAME from(version1) -> to(version2)")
By("Update DaemonSet env NODE_NAME from(version1) -> to(version2)")
newEnvs := mergeEnvVar(workload.Spec.Template.Spec.Containers[0].Env, v1.EnvVar{Name: "NODE_NAME", Value: "version2"})
workload.Spec.Template.Spec.Containers[0].Env = newEnvs
UpdateDaemonSet(workload)
@ -5297,16 +5462,12 @@ var _ = SIGDescribe("Rollout", func() {
Expect(workload.Status.UpdatedNumberScheduled).Should(BeNumerically("==", 1))
Expect(workload.Status.NumberReady).Should(BeNumerically("==", workload.Status.DesiredNumberScheduled))
Expect(*workload.Spec.UpdateStrategy.RollingUpdate.Paused).Should(BeFalse())
By("check deployment status & paused success")
By("check DaemonSet status & paused success")
// delete rollout
By("Delete rollout crd, and wait DaemonSet ready")
Expect(k8sClient.DeleteAllOf(context.TODO(), &v1alpha1.Rollout{}, client.InNamespace(namespace), client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
WaitRolloutNotFound(rollout.Name)
Expect(GetObject(workload.Name, workload)).NotTo(HaveOccurred())
fmt.Println(util.DumpJSON(workload))
workload.Spec.UpdateStrategy.RollingUpdate.Paused = utilpointer.Bool(false)
UpdateDaemonSet(workload)
By("Update deployment paused=false")
WaitDaemonSetAllPodsReady(workload)
// check daemonset