Add Redis rate limiter
This commit is contained in:
parent
c166a41c99
commit
550d96ea67
26 changed files with 2268 additions and 69 deletions
|
@ -8,6 +8,7 @@ data:
|
|||
users: |2
|
||||
dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5
|
||||
aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
|
|
82
pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml
Normal file
82
pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml
Normal file
|
@ -0,0 +1,82 @@
|
|||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: ratelimit
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
rateLimit:
|
||||
period: 1m
|
||||
average: 6
|
||||
burst: 12
|
||||
sourceCriterion:
|
||||
ipStrategy:
|
||||
excludedIPs:
|
||||
- 127.0.0.1/32
|
||||
- 192.168.1.7
|
||||
|
||||
redis:
|
||||
secret: redissecret
|
||||
endpoints:
|
||||
- "127.0.0.1:6379"
|
||||
tls:
|
||||
certSecret: tlssecret
|
||||
caSecret: casecret
|
||||
db: 0
|
||||
poolSize: 42
|
||||
maxActiveConns: 42
|
||||
readTimeout: 42s
|
||||
writeTimeout: 42s
|
||||
dialTimeout: 42s
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: redissecret
|
||||
namespace: default
|
||||
data:
|
||||
username: dXNlcg== # username: user
|
||||
password: cGFzc3dvcmQ= # password: password
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: casecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: tlssecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: test2.route
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
|
||||
routes:
|
||||
- match: Host(`foo.com`) && PathPrefix(`/will-be-limited`)
|
||||
priority: 12
|
||||
kind: Rule
|
||||
services:
|
||||
- name: whoami
|
||||
port: 80
|
||||
middlewares:
|
||||
- name: ratelimit
|
|
@ -266,7 +266,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
|||
continue
|
||||
}
|
||||
|
||||
rateLimit, err := createRateLimitMiddleware(middleware.Spec.RateLimit)
|
||||
rateLimit, err := createRateLimitMiddleware(client, middleware.Namespace, middleware.Spec.RateLimit)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error while reading rateLimit middleware")
|
||||
continue
|
||||
|
@ -686,7 +686,7 @@ func createCompressMiddleware(compress *traefikv1alpha1.Compress) *dynamic.Compr
|
|||
return c
|
||||
}
|
||||
|
||||
func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
|
||||
func createRateLimitMiddleware(client Client, namespace string, rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
|
||||
if rateLimit == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -713,9 +713,97 @@ func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.R
|
|||
rl.SourceCriterion = rateLimit.SourceCriterion
|
||||
}
|
||||
|
||||
if rateLimit.Redis != nil {
|
||||
rl.Redis = &dynamic.Redis{
|
||||
DB: rateLimit.Redis.DB,
|
||||
PoolSize: rateLimit.Redis.PoolSize,
|
||||
MinIdleConns: rateLimit.Redis.MinIdleConns,
|
||||
MaxActiveConns: rateLimit.Redis.MaxActiveConns,
|
||||
}
|
||||
rl.Redis.SetDefaults()
|
||||
|
||||
if len(rateLimit.Redis.Endpoints) > 0 {
|
||||
rl.Redis.Endpoints = rateLimit.Redis.Endpoints
|
||||
}
|
||||
|
||||
if rateLimit.Redis.TLS != nil {
|
||||
rl.Redis.TLS = &types.ClientTLS{
|
||||
InsecureSkipVerify: rateLimit.Redis.TLS.InsecureSkipVerify,
|
||||
}
|
||||
|
||||
if len(rateLimit.Redis.TLS.CASecret) > 0 {
|
||||
caSecret, err := loadCASecret(namespace, rateLimit.Redis.TLS.CASecret, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load auth ca secret: %w", err)
|
||||
}
|
||||
rl.Redis.TLS.CA = caSecret
|
||||
}
|
||||
|
||||
if len(rateLimit.Redis.TLS.CertSecret) > 0 {
|
||||
authSecretCert, authSecretKey, err := loadAuthTLSSecret(namespace, rateLimit.Redis.TLS.CertSecret, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load auth secret: %w", err)
|
||||
}
|
||||
rl.Redis.TLS.Cert = authSecretCert
|
||||
rl.Redis.TLS.Key = authSecretKey
|
||||
}
|
||||
}
|
||||
|
||||
if rateLimit.Redis.DialTimeout != nil {
|
||||
err := rl.Redis.DialTimeout.Set(rateLimit.Redis.DialTimeout.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if rateLimit.Redis.ReadTimeout != nil {
|
||||
err := rl.Redis.ReadTimeout.Set(rateLimit.Redis.ReadTimeout.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if rateLimit.Redis.WriteTimeout != nil {
|
||||
err := rl.Redis.WriteTimeout.Set(rateLimit.Redis.WriteTimeout.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if rateLimit.Redis.Secret != "" {
|
||||
var err error
|
||||
rl.Redis.Username, rl.Redis.Password, err = loadRedisCredentials(namespace, rateLimit.Redis.Secret, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
func loadRedisCredentials(namespace, secretName string, k8sClient Client) (string, string, error) {
|
||||
secret, exists, err := k8sClient.GetSecret(namespace, secretName)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, secretName, err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return "", "", fmt.Errorf("secret '%s/%s' not found", namespace, secretName)
|
||||
}
|
||||
|
||||
if secret == nil {
|
||||
return "", "", fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, secretName)
|
||||
}
|
||||
|
||||
username, usernameExists := secret.Data["username"]
|
||||
password, passwordExists := secret.Data["password"]
|
||||
if !usernameExists || !passwordExists {
|
||||
return "", "", fmt.Errorf("secret '%s/%s' must contain both username and password keys", secret.Namespace, secret.Name)
|
||||
}
|
||||
return string(username), string(password), nil
|
||||
}
|
||||
|
||||
func createRetryMiddleware(retry *traefikv1alpha1.Retry) (*dynamic.Retry, error) {
|
||||
if retry == nil {
|
||||
return nil, nil
|
||||
|
|
|
@ -1835,6 +1835,84 @@ func TestLoadIngressRoutes(t *testing.T) {
|
|||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple Ingress Route with middleware ratelimit",
|
||||
allowCrossNamespace: true,
|
||||
paths: []string{"services.yml", "with_ratelimit.yml"},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-test2-route-3c9bf014491ebdba74f7": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-test2-route-3c9bf014491ebdba74f7",
|
||||
Rule: "Host(`foo.com`) && PathPrefix(`/will-be-limited`)",
|
||||
Priority: 12,
|
||||
Middlewares: []string{"default-ratelimit"},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ratelimit": {
|
||||
RateLimit: &dynamic.RateLimit{
|
||||
Average: 6,
|
||||
Burst: 12,
|
||||
Period: ptypes.Duration(60 * time.Second),
|
||||
SourceCriterion: &dynamic.SourceCriterion{
|
||||
IPStrategy: &dynamic.IPStrategy{
|
||||
ExcludedIPs: []string{"127.0.0.1/32", "192.168.1.7"},
|
||||
},
|
||||
},
|
||||
Redis: &dynamic.Redis{
|
||||
Endpoints: []string{"127.0.0.1:6379"},
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
TLS: &types.ClientTLS{
|
||||
CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
|
||||
Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
|
||||
Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
|
||||
},
|
||||
DB: 0,
|
||||
PoolSize: 42,
|
||||
MaxActiveConns: 42,
|
||||
ReadTimeout: pointer(ptypes.Duration(42 * time.Second)),
|
||||
WriteTimeout: pointer(ptypes.Duration(42 * time.Second)),
|
||||
DialTimeout: pointer(ptypes.Duration(42 * time.Second)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-test2-route-3c9bf014491ebdba74f7": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: pointer(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Middlewares in ingress route config are normalized",
|
||||
allowCrossNamespace: true,
|
||||
|
|
|
@ -170,7 +170,7 @@ type ForwardAuth struct {
|
|||
// If not set or empty then all request headers are passed.
|
||||
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
|
||||
// TLS defines the configuration used to secure the connection to the authentication server.
|
||||
TLS *ClientTLS `json:"tls,omitempty"`
|
||||
TLS *ClientTLSWithCAOptional `json:"tls,omitempty"`
|
||||
// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
|
||||
AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"`
|
||||
// HeaderField defines a header field to store the authenticated user.
|
||||
|
@ -186,16 +186,12 @@ type ForwardAuth struct {
|
|||
PreserveRequestMethod bool `json:"preserveRequestMethod,omitempty"`
|
||||
}
|
||||
|
||||
// ClientTLS holds the client TLS configuration.
|
||||
type ClientTLS struct {
|
||||
// CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
||||
// The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
||||
CASecret string `json:"caSecret,omitempty"`
|
||||
// CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
||||
// The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
||||
CertSecret string `json:"certSecret,omitempty"`
|
||||
// InsecureSkipVerify defines whether the server certificates should be validated.
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// ClientTLSWithCAOptional holds the client TLS configuration.
|
||||
// TODO: This has to be removed once the CAOptional option is removed.
|
||||
type ClientTLSWithCAOptional struct {
|
||||
ClientTLS `json:",inline"`
|
||||
|
||||
// Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).
|
||||
CAOptional *bool `json:"caOptional,omitempty"`
|
||||
|
@ -225,6 +221,65 @@ type RateLimit struct {
|
|||
// If several strategies are defined at the same time, an error will be raised.
|
||||
// If none are set, the default is to use the request's remote address field (as an ipStrategy).
|
||||
SourceCriterion *dynamic.SourceCriterion `json:"sourceCriterion,omitempty"`
|
||||
// Redis hold the configs of Redis as bucket in rate limiter.
|
||||
Redis *Redis `json:"redis,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// Redis contains the configuration for using Redis in middleware.
|
||||
// In a Kubernetes setup, the username and password are stored in a Secret file within the same namespace as the middleware.
|
||||
type Redis struct {
|
||||
// Endpoints contains either a single address or a seed list of host:port addresses.
|
||||
// Default value is ["localhost:6379"].
|
||||
Endpoints []string `json:"endpoints,omitempty"`
|
||||
// TLS defines TLS-specific configurations, including the CA, certificate, and key,
|
||||
// which can be provided as a file path or file content.
|
||||
TLS *ClientTLS `json:"tls,omitempty"`
|
||||
// Secret defines the name of the referenced Kubernetes Secret containing Redis credentials.
|
||||
Secret string `json:"secret,omitempty"`
|
||||
// DB defines the Redis database that will be selected after connecting to the server.
|
||||
DB int `json:"db,omitempty"`
|
||||
// PoolSize defines the initial number of socket connections.
|
||||
// If the pool runs out of available connections, additional ones will be created beyond PoolSize.
|
||||
// This can be limited using MaxActiveConns.
|
||||
// // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||
PoolSize int `json:"poolSize,omitempty"`
|
||||
// MinIdleConns defines the minimum number of idle connections.
|
||||
// Default value is 0, and idle connections are not closed by default.
|
||||
MinIdleConns int `json:"minIdleConns,omitempty"`
|
||||
// MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
|
||||
// Default value is 0, meaning there is no limit.
|
||||
MaxActiveConns int `json:"maxActiveConns,omitempty"`
|
||||
// ReadTimeout defines the timeout for socket read operations.
|
||||
// Default value is 3 seconds.
|
||||
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
|
||||
// +kubebuilder:validation:XIntOrString
|
||||
ReadTimeout *intstr.IntOrString `json:"readTimeout,omitempty"`
|
||||
// WriteTimeout defines the timeout for socket write operations.
|
||||
// Default value is 3 seconds.
|
||||
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
|
||||
// +kubebuilder:validation:XIntOrString
|
||||
WriteTimeout *intstr.IntOrString `json:"writeTimeout,omitempty"`
|
||||
// DialTimeout sets the timeout for establishing new connections.
|
||||
// Default value is 5 seconds.
|
||||
// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
|
||||
// +kubebuilder:validation:XIntOrString
|
||||
DialTimeout *intstr.IntOrString `json:"dialTimeout,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// ClientTLS holds the client TLS configuration.
|
||||
type ClientTLS struct {
|
||||
// CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
|
||||
// The CA certificate is extracted from key `tls.ca` or `ca.crt`.
|
||||
CASecret string `json:"caSecret,omitempty"`
|
||||
// CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
|
||||
// The client certificate is extracted from the keys `tls.crt` and `tls.key`.
|
||||
CertSecret string `json:"certSecret,omitempty"`
|
||||
// InsecureSkipVerify defines whether the server certificates should be validated.
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
|
|
@ -146,11 +146,6 @@ func (in *ClientAuth) DeepCopy() *ClientAuth {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClientTLS) DeepCopyInto(out *ClientTLS) {
|
||||
*out = *in
|
||||
if in.CAOptional != nil {
|
||||
in, out := &in.CAOptional, &out.CAOptional
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -164,6 +159,28 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClientTLSWithCAOptional) DeepCopyInto(out *ClientTLSWithCAOptional) {
|
||||
*out = *in
|
||||
out.ClientTLS = in.ClientTLS
|
||||
if in.CAOptional != nil {
|
||||
in, out := &in.CAOptional, &out.CAOptional
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLSWithCAOptional.
|
||||
func (in *ClientTLSWithCAOptional) DeepCopy() *ClientTLSWithCAOptional {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ClientTLSWithCAOptional)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Compress) DeepCopyInto(out *Compress) {
|
||||
*out = *in
|
||||
|
@ -265,7 +282,7 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
|
|||
}
|
||||
if in.TLS != nil {
|
||||
in, out := &in.TLS, &out.TLS
|
||||
*out = new(ClientTLS)
|
||||
*out = new(ClientTLSWithCAOptional)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.AddAuthCookiesToResponse != nil {
|
||||
|
@ -1053,6 +1070,11 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) {
|
|||
*out = new(dynamic.SourceCriterion)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Redis != nil {
|
||||
in, out := &in.Redis, &out.Redis
|
||||
*out = new(Redis)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1066,6 +1088,47 @@ func (in *RateLimit) DeepCopy() *RateLimit {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Redis) DeepCopyInto(out *Redis) {
|
||||
*out = *in
|
||||
if in.Endpoints != nil {
|
||||
in, out := &in.Endpoints, &out.Endpoints
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.TLS != nil {
|
||||
in, out := &in.TLS, &out.TLS
|
||||
*out = new(ClientTLS)
|
||||
**out = **in
|
||||
}
|
||||
if in.ReadTimeout != nil {
|
||||
in, out := &in.ReadTimeout, &out.ReadTimeout
|
||||
*out = new(intstr.IntOrString)
|
||||
**out = **in
|
||||
}
|
||||
if in.WriteTimeout != nil {
|
||||
in, out := &in.WriteTimeout, &out.WriteTimeout
|
||||
*out = new(intstr.IntOrString)
|
||||
**out = **in
|
||||
}
|
||||
if in.DialTimeout != nil {
|
||||
in, out := &in.DialTimeout, &out.DialTimeout
|
||||
*out = new(intstr.IntOrString)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis.
|
||||
func (in *Redis) DeepCopy() *Redis {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Redis)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
||||
*out = *in
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue