Merge pull request #908 from kdorosh/add_gloo_upstreamRef

Gloo Upstream Ref for Upstream Config
This commit is contained in:
Stefan Prodan
2021-05-14 15:37:49 +03:00
committed by GitHub
10 changed files with 503 additions and 16 deletions
+15
View File
@@ -129,6 +129,21 @@ spec:
- Ingress
name:
type: string
upstreamRef:
description: Gloo Upstream selector
type: object
required: [ "apiVersion", "kind", "name" ]
properties:
apiVersion:
type: string
kind:
type: string
enum:
- Upstream
name:
type: string
namespace:
type: string
service:
description: Kubernetes Service spec
type: object
+15
View File
@@ -129,6 +129,21 @@ spec:
- Ingress
name:
type: string
upstreamRef:
description: Gloo Upstream selector
type: object
required: [ "apiVersion", "kind", "name" ]
properties:
apiVersion:
type: string
kind:
type: string
enum:
- Upstream
name:
type: string
namespace:
type: string
service:
description: Kubernetes Service spec
type: object
@@ -10,8 +10,8 @@ and Flagger to automate canary releases and A/B testing.
Flagger requires a Kubernetes cluster **v1.16** or newer and Gloo Edge ingress **1.6.0** or newer.
This guide was written for Flagger version **1.6.0** or higher. Prior versions of Flagger
used Gloo upstream groups to handle canaries, but newer versions of Flagger use Gloo
route tables to handle canaries as well as A/B testing.
used Gloo `UpstreamGroup`s to handle canaries, but newer versions of Flagger use Gloo
`RouteTable`s to handle canaries as well as A/B testing.
Install Gloo with Helm v3:
@@ -36,7 +36,7 @@ helm upgrade -i flagger flagger/flagger \
## Bootstrap
Flagger takes a Kubernetes deployment and optionally a horizontal pod autoscaler (HPA),
then creates a series of objects (Kubernetes deployments, ClusterIP services and Gloo route tables groups).
then creates a series of objects (Kubernetes deployments, ClusterIP services, Gloo route tables and upstreams).
These objects expose the application outside the cluster and drive the canary analysis and promotion.
Create a test namespace:
@@ -94,6 +94,14 @@ metadata:
name: podinfo
namespace: test
spec:
# upstreamRef (optional)
# defines an upstream to copy the spec from when flagger generates new upstreams.
# necessary to copy over TLS config, circuit breakers, etc. (anything nonstandard)
# upstreamRef:
# apiVersion: gloo.solo.io/v1
# kind: Upstream
# name: podinfo-upstream
# namespace: gloo-system
provider: gloo
# deployment reference
targetRef:
+15
View File
@@ -129,6 +129,21 @@ spec:
- Ingress
name:
type: string
upstreamRef:
description: Gloo Upstream selector
type: object
required: [ "apiVersion", "kind", "name" ]
properties:
apiVersion:
type: string
kind:
type: string
enum:
- Upstream
name:
type: string
namespace:
type: string
service:
description: Kubernetes Service spec
type: object
+5
View File
@@ -77,6 +77,11 @@ type CanarySpec struct {
// +optional
IngressRef *CrossNamespaceObjectReference `json:"ingressRef,omitempty"`
// Reference to Gloo Upstream resource. Upstream config is copied from
// the referenced upstream to the upstreams generated by flagger.
// +optional
UpstreamRef *CrossNamespaceObjectReference `json:"upstreamRef,omitempty"`
// Service defines how ClusterIP services, service mesh or ingress routing objects are generated
Service CanaryService `json:"service"`
@@ -411,6 +411,11 @@ func (in *CanarySpec) DeepCopyInto(out *CanarySpec) {
*out = new(CrossNamespaceObjectReference)
**out = **in
}
if in.UpstreamRef != nil {
in, out := &in.UpstreamRef, &out.UpstreamRef
*out = new(CrossNamespaceObjectReference)
**out = **in
}
in.Service.DeepCopyInto(&out.Service)
if in.Analysis != nil {
in, out := &in.Analysis, &out.Analysis
+88 -1
View File
@@ -1,6 +1,7 @@
package v1
import (
v1 "github.com/fluxcd/flagger/pkg/apis/gloo/gateway/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -16,7 +17,14 @@ type Upstream struct {
}
type UpstreamSpec struct {
Kube KubeUpstream `json:"kube,omitempty"`
Kube *KubeUpstream `json:"kube,omitempty"`
SslConfig *UpstreamSslConfig `json:"sslConfig,omitempty"`
CircuitBreakers *CircuitBreakerConfig `json:"circuitBreakers,omitempty"`
ConnectionConfig *ConnectionConfig `json:"connectionConfig,omitempty"`
UseHttp2 bool `json:"useHttp2,omitempty"`
InitialStreamWindowSize uint32 `json:"initialStreamWindowSize,omitempty"`
InitialConnectionWindowSize uint32 `json:"initialConnectionWindowSize,omitempty"`
HttpProxyHostname string `json:"httpProxyHostName,omitempty"`
}
type KubeUpstream struct {
@@ -26,6 +34,85 @@ type KubeUpstream struct {
Selector map[string]string `json:"selector,omitempty"`
}
type UpstreamSslConfig struct {
Sni string `json:"sni,omitempty"`
VerifySubjectAltName []string `json:"verifySubjectAltName,omitempty"`
Parameters *SslParameters `json:"parameters,omitempty"`
AlpnProtocols []string `json:"alpnProtocols,omitempty"`
/** SSLSecrets -- only one of these should be set */
*UpstreamSslConfig_Sds `json:"sds,omitempty"`
SecretRef *v1.ResourceRef `json:"secretRef,omitempty"`
*UpstreamSslConfig_SslFiles `json:"sslFiles,omitempty"`
}
// SSLFiles reference paths to certificates which can be read by the proxy off of its local filesystem
type UpstreamSslConfig_SslFiles struct {
TlsCert string `json:"tlsCert,omitempty"`
TlsKey string `json:"tlsKey,omitempty"`
RootCa string `json:"rootCa,omitempty"`
}
// Use secret discovery service.
type UpstreamSslConfig_Sds struct {
TargetUri string `json:"targetUri,omitempty"`
CertificatesSecretName string `json:"certificatesSecretName,omitempty"`
ValidationContextName string `json:"validationContextName,omitempty"`
/** SDSBuilder -- onle one of the following can be set */
CallCredentials *CallCredentials `json:"callCredentials,omitempty"`
ClusterName string `json:"clusterName,omitempty"`
}
type CallCredentials struct {
FileCredentialSource *CallCredentials_FileCredentialSource `json:"fileCredentialSource,omitempty"`
}
type CallCredentials_FileCredentialSource struct {
TokenFileName string `json:"tokenFileName,omitempty"`
Header string `json:"header,omitempty"`
}
type SslParameters struct {
MinimumProtocolVersion int32 `json:"minimumProtocolVersion,omitempty"`
MaximumProtocolVersion int32 `json:"maximumProtocolVersion,omitempty"`
CipherSuites []string `json:"cipherSuites,omitempty"`
EcdhCurves []string `json:"ecdhCurves,omitempty"`
}
type CircuitBreakerConfig struct {
MaxConnections uint32 `json:"maxConnections,omitempty"`
MaxPendingRequests uint32 `json:"maxPendingRequests,omitempty"`
MaxRequests uint32 `json:"maxRequests,omitempty"`
MaxRetries uint32 `json:"maxRetries,omitempty"`
}
type ConnectionConfig struct {
MaxRequestsPerConnection uint32 `json:"maxRequestsPerConnection,omitempty"`
ConnectTimeout *Duration `json:"connectTimeout,omitempty"`
TcpKeepalive *ConnectionConfig_TcpKeepAlive `json:"tcpKeepalive,omitempty"`
PerConnectionBufferLimitBytes uint32 `json:"perConnectionBufferLimitBytes,omitempty"`
CommonHttpProtocolOptions *ConnectionConfig_HttpProtocolOptions `json:"commonHttpProtocolOptions,omitempty"`
}
type ConnectionConfig_TcpKeepAlive struct {
KeepaliveProbes uint32 `json:"keepaliveProbes,omitempty"`
KeepaliveTime *Duration `json:"keepaliveTime,omitempty"`
KeepaliveInterval *Duration `json:"keepaliveInterval,omitempty"`
}
type ConnectionConfig_HttpProtocolOptions struct {
IdleTimeout *Duration `json:"idleTimeout,omitempty"`
MaxHeadersCount uint32 `json:"maxHeadersCount,omitempty"`
MaxStreamDuration *Duration `json:"maxStreamDuration,omitempty"`
HeadersWithUnderscoresAction uint32 `json:"headersWithUnderscoresAction,omitempty"`
}
type Duration struct {
Seconds int64 `json:"seconds,omitempty"`
Nanos int32 `json:"nanos,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// UpstreamList is a list of Upstream resources
+282 -1
View File
@@ -21,9 +21,162 @@ limitations under the License.
package v1
import (
gatewayv1 "github.com/fluxcd/flagger/pkg/apis/gloo/gateway/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CallCredentials) DeepCopyInto(out *CallCredentials) {
*out = *in
if in.FileCredentialSource != nil {
in, out := &in.FileCredentialSource, &out.FileCredentialSource
*out = new(CallCredentials_FileCredentialSource)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CallCredentials.
func (in *CallCredentials) DeepCopy() *CallCredentials {
if in == nil {
return nil
}
out := new(CallCredentials)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CallCredentials_FileCredentialSource) DeepCopyInto(out *CallCredentials_FileCredentialSource) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CallCredentials_FileCredentialSource.
func (in *CallCredentials_FileCredentialSource) DeepCopy() *CallCredentials_FileCredentialSource {
if in == nil {
return nil
}
out := new(CallCredentials_FileCredentialSource)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CircuitBreakerConfig) DeepCopyInto(out *CircuitBreakerConfig) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CircuitBreakerConfig.
func (in *CircuitBreakerConfig) DeepCopy() *CircuitBreakerConfig {
if in == nil {
return nil
}
out := new(CircuitBreakerConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConnectionConfig) DeepCopyInto(out *ConnectionConfig) {
*out = *in
if in.ConnectTimeout != nil {
in, out := &in.ConnectTimeout, &out.ConnectTimeout
*out = new(Duration)
**out = **in
}
if in.TcpKeepalive != nil {
in, out := &in.TcpKeepalive, &out.TcpKeepalive
*out = new(ConnectionConfig_TcpKeepAlive)
(*in).DeepCopyInto(*out)
}
if in.CommonHttpProtocolOptions != nil {
in, out := &in.CommonHttpProtocolOptions, &out.CommonHttpProtocolOptions
*out = new(ConnectionConfig_HttpProtocolOptions)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionConfig.
func (in *ConnectionConfig) DeepCopy() *ConnectionConfig {
if in == nil {
return nil
}
out := new(ConnectionConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConnectionConfig_HttpProtocolOptions) DeepCopyInto(out *ConnectionConfig_HttpProtocolOptions) {
*out = *in
if in.IdleTimeout != nil {
in, out := &in.IdleTimeout, &out.IdleTimeout
*out = new(Duration)
**out = **in
}
if in.MaxStreamDuration != nil {
in, out := &in.MaxStreamDuration, &out.MaxStreamDuration
*out = new(Duration)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionConfig_HttpProtocolOptions.
func (in *ConnectionConfig_HttpProtocolOptions) DeepCopy() *ConnectionConfig_HttpProtocolOptions {
if in == nil {
return nil
}
out := new(ConnectionConfig_HttpProtocolOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConnectionConfig_TcpKeepAlive) DeepCopyInto(out *ConnectionConfig_TcpKeepAlive) {
*out = *in
if in.KeepaliveTime != nil {
in, out := &in.KeepaliveTime, &out.KeepaliveTime
*out = new(Duration)
**out = **in
}
if in.KeepaliveInterval != nil {
in, out := &in.KeepaliveInterval, &out.KeepaliveInterval
*out = new(Duration)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionConfig_TcpKeepAlive.
func (in *ConnectionConfig_TcpKeepAlive) DeepCopy() *ConnectionConfig_TcpKeepAlive {
if in == nil {
return nil
}
out := new(ConnectionConfig_TcpKeepAlive)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Duration) DeepCopyInto(out *Duration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Duration.
func (in *Duration) DeepCopy() *Duration {
if in == nil {
return nil
}
out := new(Duration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubeUpstream) DeepCopyInto(out *KubeUpstream) {
*out = *in
@@ -47,6 +200,32 @@ func (in *KubeUpstream) DeepCopy() *KubeUpstream {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SslParameters) DeepCopyInto(out *SslParameters) {
*out = *in
if in.CipherSuites != nil {
in, out := &in.CipherSuites, &out.CipherSuites
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.EcdhCurves != nil {
in, out := &in.EcdhCurves, &out.EcdhCurves
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SslParameters.
func (in *SslParameters) DeepCopy() *SslParameters {
if in == nil {
return nil
}
out := new(SslParameters)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Upstream) DeepCopyInto(out *Upstream) {
*out = *in
@@ -110,7 +289,26 @@ func (in *UpstreamList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamSpec) DeepCopyInto(out *UpstreamSpec) {
*out = *in
in.Kube.DeepCopyInto(&out.Kube)
if in.Kube != nil {
in, out := &in.Kube, &out.Kube
*out = new(KubeUpstream)
(*in).DeepCopyInto(*out)
}
if in.SslConfig != nil {
in, out := &in.SslConfig, &out.SslConfig
*out = new(UpstreamSslConfig)
(*in).DeepCopyInto(*out)
}
if in.CircuitBreakers != nil {
in, out := &in.CircuitBreakers, &out.CircuitBreakers
*out = new(CircuitBreakerConfig)
**out = **in
}
if in.ConnectionConfig != nil {
in, out := &in.ConnectionConfig, &out.ConnectionConfig
*out = new(ConnectionConfig)
(*in).DeepCopyInto(*out)
}
return
}
@@ -123,3 +321,86 @@ func (in *UpstreamSpec) DeepCopy() *UpstreamSpec {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamSslConfig) DeepCopyInto(out *UpstreamSslConfig) {
*out = *in
if in.VerifySubjectAltName != nil {
in, out := &in.VerifySubjectAltName, &out.VerifySubjectAltName
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Parameters != nil {
in, out := &in.Parameters, &out.Parameters
*out = new(SslParameters)
(*in).DeepCopyInto(*out)
}
if in.AlpnProtocols != nil {
in, out := &in.AlpnProtocols, &out.AlpnProtocols
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.UpstreamSslConfig_Sds != nil {
in, out := &in.UpstreamSslConfig_Sds, &out.UpstreamSslConfig_Sds
*out = new(UpstreamSslConfig_Sds)
(*in).DeepCopyInto(*out)
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(gatewayv1.ResourceRef)
**out = **in
}
if in.UpstreamSslConfig_SslFiles != nil {
in, out := &in.UpstreamSslConfig_SslFiles, &out.UpstreamSslConfig_SslFiles
*out = new(UpstreamSslConfig_SslFiles)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamSslConfig.
func (in *UpstreamSslConfig) DeepCopy() *UpstreamSslConfig {
if in == nil {
return nil
}
out := new(UpstreamSslConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamSslConfig_Sds) DeepCopyInto(out *UpstreamSslConfig_Sds) {
*out = *in
if in.CallCredentials != nil {
in, out := &in.CallCredentials, &out.CallCredentials
*out = new(CallCredentials)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamSslConfig_Sds.
func (in *UpstreamSslConfig_Sds) DeepCopy() *UpstreamSslConfig_Sds {
if in == nil {
return nil
}
out := new(UpstreamSslConfig_Sds)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UpstreamSslConfig_SslFiles) DeepCopyInto(out *UpstreamSslConfig_SslFiles) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamSslConfig_SslFiles.
func (in *UpstreamSslConfig_SslFiles) DeepCopy() *UpstreamSslConfig_SslFiles {
if in == nil {
return nil
}
out := new(UpstreamSslConfig_SslFiles)
in.DeepCopyInto(out)
return out
}
+42 -11
View File
@@ -255,8 +255,12 @@ func (gr *GlooRouter) createFlaggerUpstream(canary *flaggerv1.Canary, upstreamNa
}
_, err = upstreamClient.Get(context.TODO(), upstreamName, metav1.GetOptions{})
if errors.IsNotFound(err) {
canaryUs := gr.getGlooUpstreamKubeService(canary, svc, upstreamName)
_, err := gr.glooClient.GlooV1().Upstreams(canary.Namespace).Create(context.TODO(), canaryUs, metav1.CreateOptions{})
glooUpstreamWithConfig, err := gr.getGlooConfigUpstream(canary)
if err != nil {
return err
}
canaryUs := gr.getGlooUpstreamKubeService(canary, svc, upstreamName, glooUpstreamWithConfig)
_, err = gr.glooClient.GlooV1().Upstreams(canary.Namespace).Create(context.TODO(), canaryUs, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("upstream %s.%s create query error: %w", upstreamName, canary.Namespace, err)
}
@@ -266,7 +270,29 @@ func (gr *GlooRouter) createFlaggerUpstream(canary *flaggerv1.Canary, upstreamNa
return nil
}
func (gr *GlooRouter) getGlooUpstreamKubeService(canary *flaggerv1.Canary, svc *corev1.Service, upstreamName string) *gloov1.Upstream {
func (gr *GlooRouter) getGlooUpstreamKubeService(canary *flaggerv1.Canary, svc *corev1.Service, upstreamName string, glooUpstreamWithConfig *gloov1.Upstream) *gloov1.Upstream {
upstreamSpec := gloov1.UpstreamSpec{}
if glooUpstreamWithConfig != nil {
configSpec := glooUpstreamWithConfig.Spec
upstreamSpec = gloov1.UpstreamSpec{
SslConfig: configSpec.SslConfig,
CircuitBreakers: configSpec.CircuitBreakers,
ConnectionConfig: configSpec.ConnectionConfig,
UseHttp2: configSpec.UseHttp2,
InitialStreamWindowSize: configSpec.InitialStreamWindowSize,
InitialConnectionWindowSize: configSpec.InitialConnectionWindowSize,
HttpProxyHostname: configSpec.HttpProxyHostname,
}
}
upstreamSpec.Kube = &gloov1.KubeUpstream{
ServiceName: svc.GetName(),
ServiceNamespace: canary.Namespace,
ServicePort: canary.Spec.Service.Port,
Selector: svc.Spec.Selector,
}
return &gloov1.Upstream{
ObjectMeta: metav1.ObjectMeta{
Name: upstreamName,
@@ -279,17 +305,22 @@ func (gr *GlooRouter) getGlooUpstreamKubeService(canary *flaggerv1.Canary, svc *
}),
},
},
Spec: gloov1.UpstreamSpec{
Kube: gloov1.KubeUpstream{
ServiceName: svc.GetName(),
ServiceNamespace: canary.Namespace,
ServicePort: canary.Spec.Service.Port,
Selector: svc.Spec.Selector,
},
},
Spec: upstreamSpec,
}
}
func (gr *GlooRouter) getGlooConfigUpstream(canary *flaggerv1.Canary) (*gloov1.Upstream, error) {
configUpstreamRef := canary.Spec.UpstreamRef
if configUpstreamRef == nil {
return nil, nil
}
configUpstream, err := gr.glooClient.GlooV1().Upstreams(configUpstreamRef.Namespace).Get(context.TODO(), configUpstreamRef.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("config upstream %s.%s get query error: %w", configUpstreamRef.Name, canary.Namespace, err)
}
return configUpstream, nil
}
func getMatchers(canary *flaggerv1.Canary) []gatewayv1.Matcher {
headerMatchers := getHeaderMatchers(canary)
+25
View File
@@ -25,6 +25,23 @@ spec:
namespace: test
EOF
# Create upstream that will have config that will be applied to generated flagger upstreams
# but will be used for no other reason
cat <<EOF | kubectl apply -f -
apiVersion: gloo.solo.io/v1
kind: Upstream
metadata:
name: config-upstream
namespace: gloo-system
spec:
static:
hosts:
- addr: "example.com"
port: 80
connectionConfig:
maxRequestsPerConnection: 51
EOF
cat <<EOF | kubectl apply -f -
apiVersion: flagger.app/v1beta1
kind: Canary
@@ -32,6 +49,11 @@ metadata:
name: podinfo
namespace: test
spec:
upstreamRef:
apiVersion: gloo.solo.io/v1
kind: Upstream
name: config-upstream
namespace: gloo-system
provider: gloo
targetRef:
apiVersion: apps/v1
@@ -75,6 +97,9 @@ count=0
ok=false
until ${ok}; do
kubectl -n test get canary/podinfo | grep 'Initialized' && ok=true || ok=false
if $ok ; then
kubectl -n test get upstream/test-podinfo-canaryupstream-80 -ojson | jq 'any(.spec.connectionConfig.maxRequestsPerConnection; contains(51))' && ok=true || ok=false
fi
sleep 5
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then