From 204228bc8fe367d44fffbc37f4d0e5fca2af451e Mon Sep 17 00:00:00 2001 From: John Harris Date: Wed, 15 Dec 2021 15:13:47 -0800 Subject: [PATCH 01/11] Add API types. Signed-off-by: John Harris --- hack/update-codegen.sh | 2 +- pkg/apis/kuma/register.go | 5 + pkg/apis/kuma/v1alpha1/doc.go | 5 + pkg/apis/kuma/v1alpha1/register.go | 36 +++++++ pkg/apis/kuma/v1alpha1/trafficroute.go | 41 +++++++ .../kuma/v1alpha1/zz_generated.deepcopy.go | 86 +++++++++++++++ pkg/client/clientset/versioned/clientset.go | 13 +++ .../versioned/fake/clientset_generated.go | 7 ++ .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../versioned/typed/kuma/v1alpha1/doc.go | 20 ++++ .../versioned/typed/kuma/v1alpha1/fake/doc.go | 20 ++++ .../kuma/v1alpha1/fake/fake_kuma_client.go | 35 ++++++ .../kuma/v1alpha1/generated_expansion.go | 19 ++++ .../typed/kuma/v1alpha1/kuma_client.go | 102 ++++++++++++++++++ 15 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 pkg/apis/kuma/register.go create mode 100644 pkg/apis/kuma/v1alpha1/doc.go create mode 100644 pkg/apis/kuma/v1alpha1/register.go create mode 100644 pkg/apis/kuma/v1alpha1/trafficroute.go create mode 100644 pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/doc.go create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/doc.go create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 3ed27fdc3..ecac5a467 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -30,7 +30,7 @@ chmod +x ${CODEGEN_PKG}/generate-groups.sh ${CODEGEN_PKG}/generate-groups.sh all \ github.com/fluxcd/flagger/pkg/client github.com/fluxcd/flagger/pkg/apis \ - "flagger:v1beta1 appmesh:v1beta2 appmesh:v1beta1 istio:v1alpha3 smi:v1alpha1 smi:v1alpha2 smi:v1alpha3 gloo/gloo:v1 gloo/gateway:v1 projectcontour:v1 traefik:v1alpha1" \ + "flagger:v1beta1 appmesh:v1beta2 appmesh:v1beta1 istio:v1alpha3 smi:v1alpha1 smi:v1alpha2 smi:v1alpha3 gloo/gloo:v1 gloo/gateway:v1 projectcontour:v1 traefik:v1alpha1 kuma:v1alpha1" \ --output-base "${TEMP_DIR}" \ --go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt diff --git a/pkg/apis/kuma/register.go b/pkg/apis/kuma/register.go new file mode 100644 index 000000000..fd3cb1ac5 --- /dev/null +++ b/pkg/apis/kuma/register.go @@ -0,0 +1,5 @@ +package kuma + +const ( + GroupName = "kuma.io" +) diff --git a/pkg/apis/kuma/v1alpha1/doc.go b/pkg/apis/kuma/v1alpha1/doc.go new file mode 100644 index 000000000..0cb846ef7 --- /dev/null +++ b/pkg/apis/kuma/v1alpha1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package + +// Package v1 is the v1 version of the API. +// +groupName=kuma.io +package v1alpha1 diff --git a/pkg/apis/kuma/v1alpha1/register.go b/pkg/apis/kuma/v1alpha1/register.go new file mode 100644 index 000000000..81974cc57 --- /dev/null +++ b/pkg/apis/kuma/v1alpha1/register.go @@ -0,0 +1,36 @@ +package v1alpha1 + +import ( + "github.com/fluxcd/flagger/pkg/apis/kuma" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is the GroupVersion for the Contour API +var SchemeGroupVersion = schema.GroupVersion{Group: kuma.GroupName, Version: "v1alpha1"} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource gets an Contour GroupResource for a specified resource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &TrafficRoute{}, + &TrafficRouteList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/kuma/v1alpha1/trafficroute.go b/pkg/apis/kuma/v1alpha1/trafficroute.go new file mode 100644 index 000000000..f114ddac1 --- /dev/null +++ b/pkg/apis/kuma/v1alpha1/trafficroute.go @@ -0,0 +1,41 @@ +/* +Copyright 2019 Kuma 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" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// TrafficRoute is the Schema for the Traffic Routes API. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +type TrafficRoute struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Mesh string `json:"mesh,omitempty"` + + Spec unstructured.Unstructured `json:"spec,omitempty"` +} + +// TrafficRouteList contains a list of TrafficRoutes. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +type TrafficRouteList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TrafficRoute `json:"items"` +} diff --git a/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..1c50c980a --- /dev/null +++ b/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,86 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + 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 *TrafficRoute) DeepCopyInto(out *TrafficRoute) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRoute. +func (in *TrafficRoute) DeepCopy() *TrafficRoute { + if in == nil { + return nil + } + out := new(TrafficRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TrafficRoute) 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 *TrafficRouteList) DeepCopyInto(out *TrafficRouteList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TrafficRoute, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRouteList. +func (in *TrafficRouteList) DeepCopy() *TrafficRouteList { + if in == nil { + return nil + } + out := new(TrafficRouteList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TrafficRouteList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index d923ab7b8..ecc007f5e 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -28,6 +28,7 @@ import ( gatewayv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gateway/v1" gloov1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloo/v1" networkingv1alpha3 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/istio/v1alpha3" + kumav1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/kuma/v1alpha1" projectcontourv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/projectcontour/v1" splitv1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha1" splitv1alpha2 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha2" @@ -46,6 +47,7 @@ type Interface interface { GatewayV1() gatewayv1.GatewayV1Interface GlooV1() gloov1.GlooV1Interface NetworkingV1alpha3() networkingv1alpha3.NetworkingV1alpha3Interface + KumaV1alpha1() kumav1alpha1.KumaV1alpha1Interface ProjectcontourV1() projectcontourv1.ProjectcontourV1Interface SplitV1alpha1() splitv1alpha1.SplitV1alpha1Interface SplitV1alpha2() splitv1alpha2.SplitV1alpha2Interface @@ -63,6 +65,7 @@ type Clientset struct { gatewayV1 *gatewayv1.GatewayV1Client glooV1 *gloov1.GlooV1Client networkingV1alpha3 *networkingv1alpha3.NetworkingV1alpha3Client + kumaV1alpha1 *kumav1alpha1.KumaV1alpha1Client projectcontourV1 *projectcontourv1.ProjectcontourV1Client splitV1alpha1 *splitv1alpha1.SplitV1alpha1Client splitV1alpha2 *splitv1alpha2.SplitV1alpha2Client @@ -100,6 +103,11 @@ func (c *Clientset) NetworkingV1alpha3() networkingv1alpha3.NetworkingV1alpha3In return c.networkingV1alpha3 } +// KumaV1alpha1 retrieves the KumaV1alpha1Client +func (c *Clientset) KumaV1alpha1() kumav1alpha1.KumaV1alpha1Interface { + return c.kumaV1alpha1 +} + // ProjectcontourV1 retrieves the ProjectcontourV1Client func (c *Clientset) ProjectcontourV1() projectcontourv1.ProjectcontourV1Interface { return c.projectcontourV1 @@ -189,6 +197,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, if err != nil { return nil, err } + cs.kumaV1alpha1, err = kumav1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.projectcontourV1, err = projectcontourv1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err @@ -236,6 +248,7 @@ func New(c rest.Interface) *Clientset { cs.gatewayV1 = gatewayv1.New(c) cs.glooV1 = gloov1.New(c) cs.networkingV1alpha3 = networkingv1alpha3.New(c) + cs.kumaV1alpha1 = kumav1alpha1.New(c) cs.projectcontourV1 = projectcontourv1.New(c) cs.splitV1alpha1 = splitv1alpha1.New(c) cs.splitV1alpha2 = splitv1alpha2.New(c) diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index 63e5a3609..0c8b64c87 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -32,6 +32,8 @@ import ( fakegloov1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloo/v1/fake" networkingv1alpha3 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/istio/v1alpha3" fakenetworkingv1alpha3 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/istio/v1alpha3/fake" + kumav1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/kuma/v1alpha1" + fakekumav1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake" projectcontourv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/projectcontour/v1" fakeprojectcontourv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/projectcontour/v1/fake" splitv1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha1" @@ -129,6 +131,11 @@ func (c *Clientset) NetworkingV1alpha3() networkingv1alpha3.NetworkingV1alpha3In return &fakenetworkingv1alpha3.FakeNetworkingV1alpha3{Fake: &c.Fake} } +// KumaV1alpha1 retrieves the KumaV1alpha1Client +func (c *Clientset) KumaV1alpha1() kumav1alpha1.KumaV1alpha1Interface { + return &fakekumav1alpha1.FakeKumaV1alpha1{Fake: &c.Fake} +} + // ProjectcontourV1 retrieves the ProjectcontourV1Client func (c *Clientset) ProjectcontourV1() projectcontourv1.ProjectcontourV1Interface { return &fakeprojectcontourv1.FakeProjectcontourV1{Fake: &c.Fake} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index 18145710a..f4f594b08 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -25,6 +25,7 @@ import ( gatewayv1 "github.com/fluxcd/flagger/pkg/apis/gloo/gateway/v1" gloov1 "github.com/fluxcd/flagger/pkg/apis/gloo/gloo/v1" networkingv1alpha3 "github.com/fluxcd/flagger/pkg/apis/istio/v1alpha3" + kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" projectcontourv1 "github.com/fluxcd/flagger/pkg/apis/projectcontour/v1" splitv1alpha1 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha1" splitv1alpha2 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha2" @@ -47,6 +48,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ gatewayv1.AddToScheme, gloov1.AddToScheme, networkingv1alpha3.AddToScheme, + kumav1alpha1.AddToScheme, projectcontourv1.AddToScheme, splitv1alpha1.AddToScheme, splitv1alpha2.AddToScheme, diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 0c95149c7..a50824532 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -25,6 +25,7 @@ import ( gatewayv1 "github.com/fluxcd/flagger/pkg/apis/gloo/gateway/v1" gloov1 "github.com/fluxcd/flagger/pkg/apis/gloo/gloo/v1" networkingv1alpha3 "github.com/fluxcd/flagger/pkg/apis/istio/v1alpha3" + kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" projectcontourv1 "github.com/fluxcd/flagger/pkg/apis/projectcontour/v1" splitv1alpha1 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha1" splitv1alpha2 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha2" @@ -47,6 +48,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ gatewayv1.AddToScheme, gloov1.AddToScheme, networkingv1alpha3.AddToScheme, + kumav1alpha1.AddToScheme, projectcontourv1.AddToScheme, splitv1alpha1.AddToScheme, splitv1alpha2.AddToScheme, diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/doc.go new file mode 100644 index 000000000..9c7b8cc3b --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/doc.go new file mode 100644 index 000000000..1ccd91197 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go new file mode 100644 index 000000000..a46015094 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go @@ -0,0 +1,35 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeKumaV1alpha1 struct { + *testing.Fake +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeKumaV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go new file mode 100644 index 000000000..8e2fcbe1a --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go @@ -0,0 +1,19 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go new file mode 100644 index 000000000..1da7cce66 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go @@ -0,0 +1,102 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" + "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type KumaV1alpha1Interface interface { + RESTClient() rest.Interface +} + +// KumaV1alpha1Client is used to interact with features provided by the kuma.io group. +type KumaV1alpha1Client struct { + restClient rest.Interface +} + +// NewForConfig creates a new KumaV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*KumaV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new KumaV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*KumaV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &KumaV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new KumaV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *KumaV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new KumaV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *KumaV1alpha1Client { + return &KumaV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *KumaV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} From bd58a4786255b952791bd3eb406ed96fb3205686 Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:05:54 -0800 Subject: [PATCH 02/11] Add/update API types Signed-off-by: John Harris --- pkg/apis/flagger/v1beta1/provider.go | 1 + pkg/apis/kuma/v1alpha1/register.go | 4 +- pkg/apis/kuma/v1alpha1/trafficroute.go | 59 +++++- .../kuma/v1alpha1/zz_generated.deepcopy.go | 116 ++++++++++++ .../kuma/v1alpha1/fake/fake_kuma_client.go | 5 + .../kuma/v1alpha1/fake/fake_trafficroute.go | 122 +++++++++++++ .../kuma/v1alpha1/generated_expansion.go | 2 + .../typed/kuma/v1alpha1/kuma_client.go | 5 + .../typed/kuma/v1alpha1/trafficroute.go | 168 ++++++++++++++++++ .../informers/externalversions/factory.go | 6 + .../informers/externalversions/generic.go | 9 +- .../externalversions/kuma/interface.go | 46 +++++ .../kuma/v1alpha1/interface.go | 45 +++++ .../kuma/v1alpha1/trafficroute.go | 89 ++++++++++ .../kuma/v1alpha1/expansion_generated.go | 23 +++ .../listers/kuma/v1alpha1/trafficroute.go | 68 +++++++ 16 files changed, 755 insertions(+), 13 deletions(-) create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_trafficroute.go create mode 100644 pkg/client/clientset/versioned/typed/kuma/v1alpha1/trafficroute.go create mode 100644 pkg/client/informers/externalversions/kuma/interface.go create mode 100644 pkg/client/informers/externalversions/kuma/v1alpha1/interface.go create mode 100644 pkg/client/informers/externalversions/kuma/v1alpha1/trafficroute.go create mode 100644 pkg/client/listers/kuma/v1alpha1/expansion_generated.go create mode 100644 pkg/client/listers/kuma/v1alpha1/trafficroute.go diff --git a/pkg/apis/flagger/v1beta1/provider.go b/pkg/apis/flagger/v1beta1/provider.go index fb48d13f0..dcfa8d726 100644 --- a/pkg/apis/flagger/v1beta1/provider.go +++ b/pkg/apis/flagger/v1beta1/provider.go @@ -12,4 +12,5 @@ const ( SkipperProvider string = "skipper" TraefikProvider string = "traefik" OsmProvider string = "osm" + KumaProvider string = "kuma" ) diff --git a/pkg/apis/kuma/v1alpha1/register.go b/pkg/apis/kuma/v1alpha1/register.go index 81974cc57..8a7b17b19 100644 --- a/pkg/apis/kuma/v1alpha1/register.go +++ b/pkg/apis/kuma/v1alpha1/register.go @@ -7,7 +7,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -// SchemeGroupVersion is the GroupVersion for the Contour API +// SchemeGroupVersion is the GroupVersion for the Kuma API var SchemeGroupVersion = schema.GroupVersion{Group: kuma.GroupName, Version: "v1alpha1"} // Kind takes an unqualified kind and returns back a Group qualified GroupKind @@ -15,7 +15,7 @@ func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } -// Resource gets an Contour GroupResource for a specified resource +// Resource gets a Kuma GroupResource for a specified resource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } diff --git a/pkg/apis/kuma/v1alpha1/trafficroute.go b/pkg/apis/kuma/v1alpha1/trafficroute.go index f114ddac1..175b06461 100644 --- a/pkg/apis/kuma/v1alpha1/trafficroute.go +++ b/pkg/apis/kuma/v1alpha1/trafficroute.go @@ -15,27 +15,68 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +// +genclient +// +genclient:noStatus +// +genclient:nonNamespaced + // TrafficRoute is the Schema for the Traffic Routes API. -// // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:object:root=true type TrafficRoute struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Mesh string `json:"mesh,omitempty"` - - Spec unstructured.Unstructured `json:"spec,omitempty"` + Mesh string `json:"mesh,omitempty"` + Spec TrafficRouteSpec `json:"spec,omitempty"` } -// TrafficRouteList contains a list of TrafficRoutes. -// +// TrafficRouteList defines a list of TrafficRoute objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:object:root=true type TrafficRouteList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []TrafficRoute `json:"items"` } + +// TrafficRouteSpec defines the spec for a TrafficRoute. +type TrafficRouteSpec struct { + // List of selectors to match data plane proxies that are sources of traffic. + Sources []*Selector `json:"sources,omitempty"` + // List of selectors to match services that are destinations of traffic. + // + // Notice the difference between sources and destinations. + // While the source of traffic is always a data plane proxy within a mesh, + // the destination is a service that could be either within or outside + // of a mesh. + Destinations []*Selector `json:"destinations,omitempty"` + // Configuration for the route. + Conf *TrafficRouteConf `json:"conf,omitempty"` +} + +// Selector defines the configuration for which Kuma services should be targeted. +type Selector struct { + // Tags to match, can be used for both source and destinations + Match map[string]string `json:"match,omitempty"` +} + +// TrafficRouteConf defines the destination configuration. +type TrafficRouteConf struct { + // List of destinations with weights assigned to them. + // When used, "destination" is not allowed. + Split []*TrafficRouteSplit `json:"split,omitempty"` +} + +// TrafficRouteSplit defines a destination with a weight assigned to it. +type TrafficRouteSplit struct { + // Weight assigned to that destination. + // Weights are not percentages. For example two destinations with + // weights the same weight "1" will receive both same amount of the traffic. + // 0 means that the destination will be ignored. + Weight uint32 `json:"weight"` + // Selector to match individual endpoints that comprise that destination. + // + // Notice that an endpoint can be either inside or outside the mesh. + // In the former case an endpoint corresponds to a data plane proxy, + // in the latter case an endpoint is an External Service. + Destination map[string]string `json:"destination,omitempty"` +} diff --git a/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go index 1c50c980a..8e3d40381 100644 --- a/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kuma/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,29 @@ import ( 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 *Selector) DeepCopyInto(out *Selector) { + *out = *in + if in.Match != nil { + in, out := &in.Match, &out.Match + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector. +func (in *Selector) DeepCopy() *Selector { + if in == nil { + return nil + } + out := new(Selector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TrafficRoute) DeepCopyInto(out *TrafficRoute) { *out = *in @@ -52,6 +75,33 @@ func (in *TrafficRoute) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficRouteConf) DeepCopyInto(out *TrafficRouteConf) { + *out = *in + if in.Split != nil { + in, out := &in.Split, &out.Split + *out = make([]*TrafficRouteSplit, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TrafficRouteSplit) + (*in).DeepCopyInto(*out) + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRouteConf. +func (in *TrafficRouteConf) DeepCopy() *TrafficRouteConf { + if in == nil { + return nil + } + out := new(TrafficRouteConf) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TrafficRouteList) DeepCopyInto(out *TrafficRouteList) { *out = *in @@ -84,3 +134,69 @@ func (in *TrafficRouteList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficRouteSpec) DeepCopyInto(out *TrafficRouteSpec) { + *out = *in + if in.Sources != nil { + in, out := &in.Sources, &out.Sources + *out = make([]*Selector, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Selector) + (*in).DeepCopyInto(*out) + } + } + } + if in.Destinations != nil { + in, out := &in.Destinations, &out.Destinations + *out = make([]*Selector, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Selector) + (*in).DeepCopyInto(*out) + } + } + } + if in.Conf != nil { + in, out := &in.Conf, &out.Conf + *out = new(TrafficRouteConf) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRouteSpec. +func (in *TrafficRouteSpec) DeepCopy() *TrafficRouteSpec { + if in == nil { + return nil + } + out := new(TrafficRouteSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficRouteSplit) DeepCopyInto(out *TrafficRouteSplit) { + *out = *in + if in.Destination != nil { + in, out := &in.Destination, &out.Destination + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficRouteSplit. +func (in *TrafficRouteSplit) DeepCopy() *TrafficRouteSplit { + if in == nil { + return nil + } + out := new(TrafficRouteSplit) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go index a46015094..d75ef37d4 100644 --- a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_kuma_client.go @@ -19,6 +19,7 @@ limitations under the License. package fake import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/kuma/v1alpha1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" ) @@ -27,6 +28,10 @@ type FakeKumaV1alpha1 struct { *testing.Fake } +func (c *FakeKumaV1alpha1) TrafficRoutes() v1alpha1.TrafficRouteInterface { + return &FakeTrafficRoutes{c} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeKumaV1alpha1) RESTClient() rest.Interface { diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_trafficroute.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_trafficroute.go new file mode 100644 index 000000000..51d2e1549 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/fake/fake_trafficroute.go @@ -0,0 +1,122 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeTrafficRoutes implements TrafficRouteInterface +type FakeTrafficRoutes struct { + Fake *FakeKumaV1alpha1 +} + +var trafficroutesResource = schema.GroupVersionResource{Group: "kuma.io", Version: "v1alpha1", Resource: "trafficroutes"} + +var trafficroutesKind = schema.GroupVersionKind{Group: "kuma.io", Version: "v1alpha1", Kind: "TrafficRoute"} + +// Get takes name of the trafficRoute, and returns the corresponding trafficRoute object, and an error if there is any. +func (c *FakeTrafficRoutes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.TrafficRoute, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(trafficroutesResource, name), &v1alpha1.TrafficRoute{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TrafficRoute), err +} + +// List takes label and field selectors, and returns the list of TrafficRoutes that match those selectors. +func (c *FakeTrafficRoutes) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.TrafficRouteList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(trafficroutesResource, trafficroutesKind, opts), &v1alpha1.TrafficRouteList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.TrafficRouteList{ListMeta: obj.(*v1alpha1.TrafficRouteList).ListMeta} + for _, item := range obj.(*v1alpha1.TrafficRouteList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested trafficRoutes. +func (c *FakeTrafficRoutes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(trafficroutesResource, opts)) +} + +// Create takes the representation of a trafficRoute and creates it. Returns the server's representation of the trafficRoute, and an error, if there is any. +func (c *FakeTrafficRoutes) Create(ctx context.Context, trafficRoute *v1alpha1.TrafficRoute, opts v1.CreateOptions) (result *v1alpha1.TrafficRoute, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(trafficroutesResource, trafficRoute), &v1alpha1.TrafficRoute{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TrafficRoute), err +} + +// Update takes the representation of a trafficRoute and updates it. Returns the server's representation of the trafficRoute, and an error, if there is any. +func (c *FakeTrafficRoutes) Update(ctx context.Context, trafficRoute *v1alpha1.TrafficRoute, opts v1.UpdateOptions) (result *v1alpha1.TrafficRoute, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(trafficroutesResource, trafficRoute), &v1alpha1.TrafficRoute{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TrafficRoute), err +} + +// Delete takes name of the trafficRoute and deletes it. Returns an error if one occurs. +func (c *FakeTrafficRoutes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(trafficroutesResource, name, opts), &v1alpha1.TrafficRoute{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeTrafficRoutes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(trafficroutesResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.TrafficRouteList{}) + return err +} + +// Patch applies the patch and returns the patched trafficRoute. +func (c *FakeTrafficRoutes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TrafficRoute, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(trafficroutesResource, name, pt, data, subresources...), &v1alpha1.TrafficRoute{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TrafficRoute), err +} diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go index 8e2fcbe1a..b8a7a3e72 100644 --- a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/generated_expansion.go @@ -17,3 +17,5 @@ limitations under the License. // Code generated by client-gen. DO NOT EDIT. package v1alpha1 + +type TrafficRouteExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go index 1da7cce66..1fdbd2b9e 100644 --- a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/kuma_client.go @@ -28,6 +28,7 @@ import ( type KumaV1alpha1Interface interface { RESTClient() rest.Interface + TrafficRoutesGetter } // KumaV1alpha1Client is used to interact with features provided by the kuma.io group. @@ -35,6 +36,10 @@ type KumaV1alpha1Client struct { restClient rest.Interface } +func (c *KumaV1alpha1Client) TrafficRoutes() TrafficRouteInterface { + return newTrafficRoutes(c) +} + // NewForConfig creates a new KumaV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/pkg/client/clientset/versioned/typed/kuma/v1alpha1/trafficroute.go b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/trafficroute.go new file mode 100644 index 000000000..76089aba3 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/kuma/v1alpha1/trafficroute.go @@ -0,0 +1,168 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" + scheme "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// TrafficRoutesGetter has a method to return a TrafficRouteInterface. +// A group's client should implement this interface. +type TrafficRoutesGetter interface { + TrafficRoutes() TrafficRouteInterface +} + +// TrafficRouteInterface has methods to work with TrafficRoute resources. +type TrafficRouteInterface interface { + Create(ctx context.Context, trafficRoute *v1alpha1.TrafficRoute, opts v1.CreateOptions) (*v1alpha1.TrafficRoute, error) + Update(ctx context.Context, trafficRoute *v1alpha1.TrafficRoute, opts v1.UpdateOptions) (*v1alpha1.TrafficRoute, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.TrafficRoute, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.TrafficRouteList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TrafficRoute, err error) + TrafficRouteExpansion +} + +// trafficRoutes implements TrafficRouteInterface +type trafficRoutes struct { + client rest.Interface +} + +// newTrafficRoutes returns a TrafficRoutes +func newTrafficRoutes(c *KumaV1alpha1Client) *trafficRoutes { + return &trafficRoutes{ + client: c.RESTClient(), + } +} + +// Get takes name of the trafficRoute, and returns the corresponding trafficRoute object, and an error if there is any. +func (c *trafficRoutes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.TrafficRoute, err error) { + result = &v1alpha1.TrafficRoute{} + err = c.client.Get(). + Resource("trafficroutes"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of TrafficRoutes that match those selectors. +func (c *trafficRoutes) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.TrafficRouteList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.TrafficRouteList{} + err = c.client.Get(). + Resource("trafficroutes"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested trafficRoutes. +func (c *trafficRoutes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("trafficroutes"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a trafficRoute and creates it. Returns the server's representation of the trafficRoute, and an error, if there is any. +func (c *trafficRoutes) Create(ctx context.Context, trafficRoute *v1alpha1.TrafficRoute, opts v1.CreateOptions) (result *v1alpha1.TrafficRoute, err error) { + result = &v1alpha1.TrafficRoute{} + err = c.client.Post(). + Resource("trafficroutes"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(trafficRoute). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a trafficRoute and updates it. Returns the server's representation of the trafficRoute, and an error, if there is any. +func (c *trafficRoutes) Update(ctx context.Context, trafficRoute *v1alpha1.TrafficRoute, opts v1.UpdateOptions) (result *v1alpha1.TrafficRoute, err error) { + result = &v1alpha1.TrafficRoute{} + err = c.client.Put(). + Resource("trafficroutes"). + Name(trafficRoute.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(trafficRoute). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the trafficRoute and deletes it. Returns an error if one occurs. +func (c *trafficRoutes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("trafficroutes"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *trafficRoutes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("trafficroutes"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched trafficRoute. +func (c *trafficRoutes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TrafficRoute, err error) { + result = &v1alpha1.TrafficRoute{} + err = c.client.Patch(pt). + Resource("trafficroutes"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index a876cbe4b..a4bd8fc0a 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -30,6 +30,7 @@ import ( gloo "github.com/fluxcd/flagger/pkg/client/informers/externalversions/gloo" internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" istio "github.com/fluxcd/flagger/pkg/client/informers/externalversions/istio" + kuma "github.com/fluxcd/flagger/pkg/client/informers/externalversions/kuma" projectcontour "github.com/fluxcd/flagger/pkg/client/informers/externalversions/projectcontour" smi "github.com/fluxcd/flagger/pkg/client/informers/externalversions/smi" traefik "github.com/fluxcd/flagger/pkg/client/informers/externalversions/traefik" @@ -184,6 +185,7 @@ type SharedInformerFactory interface { Gateway() gateway.Interface Gloo() gloo.Interface Networking() istio.Interface + Kuma() kuma.Interface Projectcontour() projectcontour.Interface Split() smi.Interface Traefik() traefik.Interface @@ -209,6 +211,10 @@ func (f *sharedInformerFactory) Networking() istio.Interface { return istio.New(f, f.namespace, f.tweakListOptions) } +func (f *sharedInformerFactory) Kuma() kuma.Interface { + return kuma.New(f, f.namespace, f.tweakListOptions) +} + func (f *sharedInformerFactory) Projectcontour() projectcontour.Interface { return projectcontour.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 05b56a26d..13bb2c339 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -27,8 +27,9 @@ import ( v1 "github.com/fluxcd/flagger/pkg/apis/gloo/gateway/v1" gloov1 "github.com/fluxcd/flagger/pkg/apis/gloo/gloo/v1" v1alpha3 "github.com/fluxcd/flagger/pkg/apis/istio/v1alpha3" + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" projectcontourv1 "github.com/fluxcd/flagger/pkg/apis/projectcontour/v1" - v1alpha1 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha1" + smiv1alpha1 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha1" v1alpha2 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha2" smiv1alpha3 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha3" traefikv1alpha1 "github.com/fluxcd/flagger/pkg/apis/traefik/v1alpha1" @@ -94,6 +95,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case gloov1.SchemeGroupVersion.WithResource("upstreams"): return &genericInformer{resource: resource.GroupResource(), informer: f.Gloo().V1().Upstreams().Informer()}, nil + // Group=kuma.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("trafficroutes"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Kuma().V1alpha1().TrafficRoutes().Informer()}, nil + // Group=networking.istio.io, Version=v1alpha3 case v1alpha3.SchemeGroupVersion.WithResource("destinationrules"): return &genericInformer{resource: resource.GroupResource(), informer: f.Networking().V1alpha3().DestinationRules().Informer()}, nil @@ -105,7 +110,7 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Projectcontour().V1().HTTPProxies().Informer()}, nil // Group=split.smi-spec.io, Version=v1alpha1 - case v1alpha1.SchemeGroupVersion.WithResource("trafficsplits"): + case smiv1alpha1.SchemeGroupVersion.WithResource("trafficsplits"): return &genericInformer{resource: resource.GroupResource(), informer: f.Split().V1alpha1().TrafficSplits().Informer()}, nil // Group=split.smi-spec.io, Version=v1alpha2 diff --git a/pkg/client/informers/externalversions/kuma/interface.go b/pkg/client/informers/externalversions/kuma/interface.go new file mode 100644 index 000000000..757fbc9c7 --- /dev/null +++ b/pkg/client/informers/externalversions/kuma/interface.go @@ -0,0 +1,46 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package kuma + +import ( + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/fluxcd/flagger/pkg/client/informers/externalversions/kuma/v1alpha1" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/kuma/v1alpha1/interface.go b/pkg/client/informers/externalversions/kuma/v1alpha1/interface.go new file mode 100644 index 000000000..6e51c0c40 --- /dev/null +++ b/pkg/client/informers/externalversions/kuma/v1alpha1/interface.go @@ -0,0 +1,45 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // TrafficRoutes returns a TrafficRouteInformer. + TrafficRoutes() TrafficRouteInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// TrafficRoutes returns a TrafficRouteInformer. +func (v *version) TrafficRoutes() TrafficRouteInformer { + return &trafficRouteInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/kuma/v1alpha1/trafficroute.go b/pkg/client/informers/externalversions/kuma/v1alpha1/trafficroute.go new file mode 100644 index 000000000..a2f46b94c --- /dev/null +++ b/pkg/client/informers/externalversions/kuma/v1alpha1/trafficroute.go @@ -0,0 +1,89 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" + versioned "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/fluxcd/flagger/pkg/client/listers/kuma/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// TrafficRouteInformer provides access to a shared informer and lister for +// TrafficRoutes. +type TrafficRouteInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.TrafficRouteLister +} + +type trafficRouteInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewTrafficRouteInformer constructs a new informer for TrafficRoute type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTrafficRouteInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredTrafficRouteInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredTrafficRouteInformer constructs a new informer for TrafficRoute type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredTrafficRouteInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KumaV1alpha1().TrafficRoutes().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KumaV1alpha1().TrafficRoutes().Watch(context.TODO(), options) + }, + }, + &kumav1alpha1.TrafficRoute{}, + resyncPeriod, + indexers, + ) +} + +func (f *trafficRouteInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredTrafficRouteInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *trafficRouteInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&kumav1alpha1.TrafficRoute{}, f.defaultInformer) +} + +func (f *trafficRouteInformer) Lister() v1alpha1.TrafficRouteLister { + return v1alpha1.NewTrafficRouteLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/listers/kuma/v1alpha1/expansion_generated.go b/pkg/client/listers/kuma/v1alpha1/expansion_generated.go new file mode 100644 index 000000000..6e7c60482 --- /dev/null +++ b/pkg/client/listers/kuma/v1alpha1/expansion_generated.go @@ -0,0 +1,23 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// TrafficRouteListerExpansion allows custom methods to be added to +// TrafficRouteLister. +type TrafficRouteListerExpansion interface{} diff --git a/pkg/client/listers/kuma/v1alpha1/trafficroute.go b/pkg/client/listers/kuma/v1alpha1/trafficroute.go new file mode 100644 index 000000000..d6869f678 --- /dev/null +++ b/pkg/client/listers/kuma/v1alpha1/trafficroute.go @@ -0,0 +1,68 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// TrafficRouteLister helps list TrafficRoutes. +// All objects returned here must be treated as read-only. +type TrafficRouteLister interface { + // List lists all TrafficRoutes in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.TrafficRoute, err error) + // Get retrieves the TrafficRoute from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.TrafficRoute, error) + TrafficRouteListerExpansion +} + +// trafficRouteLister implements the TrafficRouteLister interface. +type trafficRouteLister struct { + indexer cache.Indexer +} + +// NewTrafficRouteLister returns a new TrafficRouteLister. +func NewTrafficRouteLister(indexer cache.Indexer) TrafficRouteLister { + return &trafficRouteLister{indexer: indexer} +} + +// List lists all TrafficRoutes in the indexer. +func (s *trafficRouteLister) List(selector labels.Selector) (ret []*v1alpha1.TrafficRoute, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.TrafficRoute)) + }) + return ret, err +} + +// Get retrieves the TrafficRoute from the index for a given name. +func (s *trafficRouteLister) Get(name string) (*v1alpha1.TrafficRoute, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("trafficroute"), name) + } + return obj.(*v1alpha1.TrafficRoute), nil +} From 6832a4ffdef9ebd4f1aa89defd434ecf928566fc Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:07:05 -0800 Subject: [PATCH 03/11] Add/update Kustomize configurations Signed-off-by: John Harris --- kustomize/base/flagger/deployment.yaml | 1 - kustomize/base/flagger/rbac.yaml | 13 +++++++++++++ kustomize/kuma/kustomization.yaml | 5 +++++ kustomize/kuma/patch.yaml | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 kustomize/kuma/kustomization.yaml create mode 100644 kustomize/kuma/patch.yaml diff --git a/kustomize/base/flagger/deployment.yaml b/kustomize/base/flagger/deployment.yaml index f0bd8b133..af0c3f608 100644 --- a/kustomize/base/flagger/deployment.yaml +++ b/kustomize/base/flagger/deployment.yaml @@ -53,5 +53,4 @@ spec: memory: "32Mi" cpu: "10m" securityContext: - readOnlyRootFilesystem: true runAsUser: 10001 diff --git a/kustomize/base/flagger/rbac.yaml b/kustomize/base/flagger/rbac.yaml index 354de8330..bb1134a42 100644 --- a/kustomize/base/flagger/rbac.yaml +++ b/kustomize/base/flagger/rbac.yaml @@ -177,6 +177,19 @@ rules: - update - patch - delete + - apiGroups: + - kuma.io + resources: + - trafficroutes + - trafficroutes/finalizers + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - nonResourceURLs: - /version verbs: diff --git a/kustomize/kuma/kustomization.yaml b/kustomize/kuma/kustomization.yaml new file mode 100644 index 000000000..bdd2d17a1 --- /dev/null +++ b/kustomize/kuma/kustomization.yaml @@ -0,0 +1,5 @@ +namespace: kuma-system +bases: + - ../base/flagger/ +patchesStrategicMerge: + - patch.yaml diff --git a/kustomize/kuma/patch.yaml b/kustomize/kuma/patch.yaml new file mode 100644 index 000000000..78c084012 --- /dev/null +++ b/kustomize/kuma/patch.yaml @@ -0,0 +1,14 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flagger +spec: + template: + spec: + containers: + - name: flagger + args: + - -log-level=info + - -include-label-prefix=app.kubernetes.io + - -mesh-provider=kuma + - -metrics-server=http://prometheus-server.kuma-metrics:80 \ No newline at end of file From 47be2a25f29a84229355e8cc5c75546f7720c261 Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:07:59 -0800 Subject: [PATCH 04/11] Add Kuma routing and metrics Signed-off-by: John Harris --- pkg/metrics/observers/factory.go | 4 + pkg/metrics/observers/kuma.go | 94 +++++++++++++ pkg/metrics/observers/kuma_test.go | 100 ++++++++++++++ pkg/router/factory.go | 7 + pkg/router/kuma.go | 212 +++++++++++++++++++++++++++++ pkg/router/kuma_test.go | 78 +++++++++++ 6 files changed, 495 insertions(+) create mode 100644 pkg/metrics/observers/kuma.go create mode 100644 pkg/metrics/observers/kuma_test.go create mode 100644 pkg/router/kuma.go create mode 100644 pkg/router/kuma_test.go diff --git a/pkg/metrics/observers/factory.go b/pkg/metrics/observers/factory.go index 9b54dbe51..23e4bd2f4 100644 --- a/pkg/metrics/observers/factory.go +++ b/pkg/metrics/observers/factory.go @@ -84,6 +84,10 @@ func (factory Factory) Observer(provider string) Interface { return &OsmObserver{ client: factory.Client, } + case provider == flaggerv1.KumaProvider: + return &KumaObserver{ + client: factory.Client, + } default: return &IstioObserver{ client: factory.Client, diff --git a/pkg/metrics/observers/kuma.go b/pkg/metrics/observers/kuma.go new file mode 100644 index 000000000..699fea04a --- /dev/null +++ b/pkg/metrics/observers/kuma.go @@ -0,0 +1,94 @@ +/* +Copyright 2021 The Flux 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 observers + +import ( + "fmt" + "time" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + "github.com/fluxcd/flagger/pkg/metrics/providers" +) + +// TODO [@johnharris85]: Do we also need to select by mesh here? These could be duplicated (but in different meshes). +// We're currently getting the mesh name from an annotation on the Canary object, but that isn't propagated to the +// MetricTemplateModel. +var kumaQueries = map[string]string{ + "request-success-rate": ` + sum( + rate( + envoy_cluster_upstream_rq{ + envoy_cluster_name=~"{{ target }}-canary_{{ namespace }}_svc_[0-9a-zA-Z-]+", + envoy_response_code!~"5.*" + }[{{ interval }}] + ) + ) + / + sum( + rate( + envoy_cluster_upstream_rq{ + envoy_cluster_name=~"{{ target }}-canary_{{ namespace }}_svc_[0-9a-zA-Z-]+", + }[{{ interval }}] + ) + ) + * 100`, + "request-duration": ` + histogram_quantile( + 0.99, + sum( + rate( + envoy_cluster_upstream_rq_time_bucket{ + envoy_cluster_name=~"{{ target }}-canary_{{ namespace }}_svc_[0-9a-zA-Z-]+", + }[{{ interval }}] + ) + ) by (le) + )`, +} + +type KumaObserver struct { + client providers.Interface +} + +func (ob *KumaObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) { + query, err := RenderQuery(kumaQueries["request-success-rate"], model) + + if err != nil { + return 0, fmt.Errorf("rendering query failed: %w", err) + } + + value, err := ob.client.RunQuery(query) + if err != nil { + return 0, fmt.Errorf("running query failed: %w", err) + } + + return value, nil +} + +func (ob *KumaObserver) GetRequestDuration(model flaggerv1.MetricTemplateModel) (time.Duration, error) { + query, err := RenderQuery(kumaQueries["request-duration"], model) + if err != nil { + return 0, fmt.Errorf("rendering query failed: %w", err) + } + + value, err := ob.client.RunQuery(query) + if err != nil { + return 0, fmt.Errorf("running query failed: %w", err) + } + + ms := time.Duration(int64(value)) * time.Millisecond + return ms, nil +} diff --git a/pkg/metrics/observers/kuma_test.go b/pkg/metrics/observers/kuma_test.go new file mode 100644 index 000000000..dda802a11 --- /dev/null +++ b/pkg/metrics/observers/kuma_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2021 The Flux 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 observers + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + "github.com/fluxcd/flagger/pkg/metrics/providers" +) + +func TestKumaObserver_GetRequestSuccessRate(t *testing.T) { + expected := ` sum( rate( envoy_cluster_upstream_rq{ envoy_cluster_name=~"podinfo-canary_default_svc_[0-9a-zA-Z-]+", envoy_response_code!~"5.*" }[1m] ) ) / sum( rate( envoy_cluster_upstream_rq{ envoy_cluster_name=~"podinfo-canary_default_svc_[0-9a-zA-Z-]+", }[1m] ) ) * 100` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + promql := r.URL.Query()["query"][0] + assert.Equal(t, expected, promql) + + json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}` + w.Write([]byte(json)) + })) + defer ts.Close() + + client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{ + Type: "prometheus", + Address: ts.URL, + SecretRef: nil, + }, nil) + require.NoError(t, err) + + observer := &KumaObserver{ + client: client, + } + + val, err := observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{ + Name: "podinfo", + Namespace: "default", + Target: "podinfo", + Service: "podinfo", + Interval: "1m", + }) + require.NoError(t, err) + + assert.Equal(t, float64(100), val) +} + +func TestKumaObserver_GetRequestDuration(t *testing.T) { + expected := ` histogram_quantile( 0.99, sum( rate( envoy_cluster_upstream_rq_time_bucket{ envoy_cluster_name=~"podinfo-canary_default_svc_[0-9a-zA-Z-]+", }[1m] ) ) by (le) )` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + promql := r.URL.Query()["query"][0] + assert.Equal(t, expected, promql) + + json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}` + w.Write([]byte(json)) + })) + defer ts.Close() + + client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{ + Type: "prometheus", + Address: ts.URL, + SecretRef: nil, + }, nil) + require.NoError(t, err) + + observer := &KumaObserver{ + client: client, + } + + val, err := observer.GetRequestDuration(flaggerv1.MetricTemplateModel{ + Name: "podinfo", + Namespace: "default", + Target: "podinfo", + Service: "podinfo", + Interval: "1m", + }) + require.NoError(t, err) + + assert.Equal(t, 100*time.Millisecond, val) +} diff --git a/pkg/router/factory.go b/pkg/router/factory.go index 5b7adc253..e5a864e74 100644 --- a/pkg/router/factory.go +++ b/pkg/router/factory.go @@ -170,6 +170,13 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf smiClient: factory.meshClient, targetMesh: flaggerv1.OsmProvider, } + case provider == flaggerv1.KumaProvider: + return &KumaRouter{ + logger: factory.logger, + flaggerClient: factory.flaggerClient, + kubeClient: factory.kubeClient, + kumaClient: factory.meshClient, + } case provider == flaggerv1.KubernetesProvider: return &NopRouter{} default: diff --git a/pkg/router/kuma.go b/pkg/router/kuma.go new file mode 100644 index 000000000..75b1b0fa6 --- /dev/null +++ b/pkg/router/kuma.go @@ -0,0 +1,212 @@ +package router + +import ( + "context" + "fmt" + "strings" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "go.uber.org/zap" + "k8s.io/client-go/kubernetes" + + clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned" +) + +// KumaRouter is managing TrafficRoute objects +type KumaRouter struct { + kubeClient kubernetes.Interface + kumaClient clientset.Interface + flaggerClient clientset.Interface + logger *zap.SugaredLogger +} + +// Reconcile creates or updates the Kuma TrafficRoute +func (kr *KumaRouter) Reconcile(canary *flaggerv1.Canary) error { + apexName, primaryName, canaryName := canary.GetServiceNames() + + trSpec := kumav1alpha1.TrafficRouteSpec{ + Sources: []*kumav1alpha1.Selector{ + { + Match: map[string]string{ + "kuma.io/service": "*", + }, + }, + }, + Destinations: []*kumav1alpha1.Selector{ + { + Match: map[string]string{ + "kuma.io/service": fmt.Sprintf("%s_%s_svc_%d", apexName, canary.Namespace, canary.Spec.Service.Port), + }, + }, + }, + Conf: &kumav1alpha1.TrafficRouteConf{ + Split: []*kumav1alpha1.TrafficRouteSplit{ + { + Weight: uint32(100), + Destination: map[string]string{ + "kuma.io/service": fmt.Sprintf("%s_%s_svc_%d", primaryName, canary.Namespace, canary.Spec.Service.Port), + }, + }, + { + Weight: uint32(0), + Destination: map[string]string{ + "kuma.io/service": fmt.Sprintf("%s_%s_svc_%d", canaryName, canary.Namespace, canary.Spec.Service.Port), + }, + }, + }, + }, + } + + tr, err := kr.kumaClient.KumaV1alpha1().TrafficRoutes().Get(context.TODO(), apexName, metav1.GetOptions{}) + + // create TrafficRoute + if errors.IsNotFound(err) { + metadata := canary.Spec.Service.Apex + if metadata == nil { + metadata = &flaggerv1.CustomMetadata{} + } + if metadata.Labels == nil { + metadata.Labels = make(map[string]string) + } + if metadata.Annotations == nil { + metadata.Annotations = make(map[string]string) + metadata.Annotations[fmt.Sprintf("%d.service.kuma.io", canary.Spec.Service.Port)] = "http" + } + + meshName, ok := canary.Annotations["kuma.io/mesh"] + if !ok { + meshName = "default" + } + + t := &kumav1alpha1.TrafficRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: apexName, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(canary, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryKind, + }), + }, + Annotations: filterMetadata(metadata.Annotations), + }, + Spec: trSpec, + Mesh: meshName, + } + + _, err := kr.kumaClient.KumaV1alpha1().TrafficRoutes().Create(context.TODO(), t, metav1.CreateOptions{}) + + if err != nil { + return fmt.Errorf("TrafficRoute %s create error: %w", apexName, err) + } + + kr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). + Infof("TrafficRoute %s created", t.GetName()) + return nil + } else if err != nil { + return fmt.Errorf("TrafficRoute %s get query error: %w", apexName, err) + } + + // update TrafficRoute + if diff := cmp.Diff(trSpec, tr.Spec, cmpopts.IgnoreFields(kumav1alpha1.TrafficRouteSplit{}, "Weight")); diff != "" { + trClone := tr.DeepCopy() + trClone.Spec = trSpec + + _, err := kr.kumaClient.KumaV1alpha1().TrafficRoutes().Update(context.TODO(), trClone, metav1.UpdateOptions{}) + + if err != nil { + return fmt.Errorf("TrafficRoute %s update error: %w", apexName, err) + } + + kr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). + Infof("TrafficRoute %s.%s updated", apexName, canary.Namespace) + return nil + } + + return nil +} + +// GetRoutes returns the destinations weight for primary and canary +func (kr *KumaRouter) GetRoutes(canary *flaggerv1.Canary) ( + primaryWeight int, + canaryWeight int, + mirrored bool, + err error, +) { + apexName, primaryName, canaryName := canary.GetServiceNames() + tr, err := kr.kumaClient.KumaV1alpha1().TrafficRoutes().Get(context.TODO(), apexName, metav1.GetOptions{}) + + if err != nil { + err = fmt.Errorf("TrafficRoute %s get query error %v", apexName, err) + return + } + + for _, split := range tr.Spec.Conf.Split { + if strings.Split(split.Destination["kuma.io/service"], "_")[0] == primaryName { + primaryWeight = int(split.Weight) + canaryWeight = 100 - primaryWeight + } + } + + if primaryWeight == 0 && canaryWeight == 0 { + err = fmt.Errorf("TrafficRoute %s does not contain routes for %s and %s", + apexName, primaryName, canaryName) + } + + mirrored = false + + return +} + +// SetRoutes updates the destinations weight for primary and canary +func (kr *KumaRouter) SetRoutes( + canary *flaggerv1.Canary, + primaryWeight int, + canaryWeight int, + _ bool, +) error { + apexName, primaryName, canaryName := canary.GetServiceNames() + tr, err := kr.kumaClient.KumaV1alpha1().TrafficRoutes().Get(context.TODO(), apexName, metav1.GetOptions{}) + + if err != nil { + return fmt.Errorf("TrafficRoute %s get query error %v", apexName, err) + } + + conf := &kumav1alpha1.TrafficRouteConf{ + Split: []*kumav1alpha1.TrafficRouteSplit{ + { + Weight: uint32(primaryWeight), + Destination: map[string]string{ + "kuma.io/service": fmt.Sprintf("%s_%s_svc_%d", primaryName, canary.Namespace, canary.Spec.Service.Port), + }, + }, + { + Weight: uint32(canaryWeight), + Destination: map[string]string{ + "kuma.io/service": fmt.Sprintf("%s_%s_svc_%d", canaryName, canary.Namespace, canary.Spec.Service.Port), + }, + }, + }, + } + + trClone := tr.DeepCopy() + trClone.Spec.Conf = conf + + _, err = kr.kumaClient.KumaV1alpha1().TrafficRoutes().Update(context.TODO(), trClone, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("TrafficRoute %s update error %v", apexName, err) + } + + return nil +} + +func (kr *KumaRouter) Finalize(_ *flaggerv1.Canary) error { + return nil +} diff --git a/pkg/router/kuma_test.go b/pkg/router/kuma_test.go new file mode 100644 index 000000000..52f10b89f --- /dev/null +++ b/pkg/router/kuma_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 The Flux 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 router + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestKumaRouter_Reconcile(t *testing.T) { + canary := newTestSMICanary() + mocks := newFixture(canary) + router := &KumaRouter{ + logger: mocks.logger, + flaggerClient: mocks.flaggerClient, + kumaClient: mocks.meshClient, + kubeClient: mocks.kubeClient, + } + + // init + err := router.Reconcile(canary) + require.NoError(t, err) + + // test insert + trafficRoute, err := router.kumaClient.KumaV1alpha1().TrafficRoutes().Get(context.TODO(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + + splits := trafficRoute.Spec.Conf.Split + require.Len(t, splits, 2) + assert.Equal(t, uint32(100), splits[0].Weight) + assert.Equal(t, uint32(0), splits[1].Weight) + +} + +func TestKumaRouter_Routes(t *testing.T) { + canary := newTestSMICanary() + mocks := newFixture(canary) + router := &KumaRouter{ + logger: mocks.logger, + flaggerClient: mocks.flaggerClient, + kumaClient: mocks.meshClient, + kubeClient: mocks.kubeClient, + } + + // init + err := router.Reconcile(canary) + require.NoError(t, err) + + // test set routers + err = router.SetRoutes(canary, 50, 50, false) + require.NoError(t, err) + + trafficRoute, err := router.kumaClient.KumaV1alpha1().TrafficRoutes().Get(context.TODO(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + + primary := trafficRoute.Spec.Conf.Split[0] + assert.Equal(t, uint32(50), primary.Weight) + +} From e81627a96d6d9ebab8fda2bf438e92515317ec4a Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:08:21 -0800 Subject: [PATCH 05/11] Add tests Signed-off-by: John Harris --- .github/workflows/e2e.yaml | 1 + test/kuma/install.sh | 61 ++++++++++++ test/kuma/run.sh | 11 +++ test/kuma/test-canary.sh | 196 +++++++++++++++++++++++++++++++++++++ test/workloads/init.sh | 1 + 5 files changed, 270 insertions(+) create mode 100755 test/kuma/install.sh create mode 100755 test/kuma/run.sh create mode 100755 test/kuma/test-canary.sh diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index da3560eae..1bf56ad03 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -23,6 +23,7 @@ jobs: - gloo - skipper - osm + - kuma - kubernetes steps: - name: Checkout diff --git a/test/kuma/install.sh b/test/kuma/install.sh new file mode 100755 index 000000000..fe121e628 --- /dev/null +++ b/test/kuma/install.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -o errexit + +KUMA_VER="1.4.1" +REPO_ROOT=$(git rev-parse --show-toplevel) +mkdir -p ${REPO_ROOT}/bin + +echo ">>> Downloading Kuma ${KUMA_VER}" +curl -SsL https://download.konghq.com/mesh-alpine/kuma-${KUMA_VER}-ubuntu-amd64.tar.gz -o kuma-${KUMA_VER}.tar.gz +tar xvzf kuma-${KUMA_VER}.tar.gz +cp kuma-${KUMA_VER}/bin/kumactl ${REPO_ROOT}/bin/kumactl +chmod +x ${REPO_ROOT}/bin/kumactl + +echo ">>> Installing Kuma ${KUMA_VER}" +${REPO_ROOT}/bin/kumactl install control-plane | kubectl apply -f - + +echo ">>> Installing Kuma Metrics" +${REPO_ROOT}/bin/kumactl install metrics | kubectl apply -f - + +echo ">>> Waiting for Kuma Control Plane to be ready" +kubectl wait --for=condition=ready pod -n kuma-system -l app=kuma-control-plane + +echo ">>> Configuring Default Kuma Mesh" +cat <>> Installing Flagger' +kubectl apply -k ${REPO_ROOT}/kustomize/kuma + +kubectl -n kuma-system set image deployment/flagger flagger=test/flagger:latest +kubectl -n kuma-system rollout status deployment/flagger diff --git a/test/kuma/run.sh b/test/kuma/run.sh new file mode 100755 index 000000000..fff250d6f --- /dev/null +++ b/test/kuma/run.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -o errexit + +REPO_ROOT=$(git rev-parse --show-toplevel) +DIR="$(cd "$(dirname "$0")" && pwd)" + +"$DIR"/install.sh + +"$REPO_ROOT"/test/workloads/init.sh +"$DIR"/test-canary.sh \ No newline at end of file diff --git a/test/kuma/test-canary.sh b/test/kuma/test-canary.sh new file mode 100755 index 000000000..68ce67caa --- /dev/null +++ b/test/kuma/test-canary.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash + +# This script runs Kuma e2e tests for Canary initialization, analysis and promotion + +set -o errexit + +REPO_ROOT=$(git rev-parse --show-toplevel) + +cat <>> Waiting for primary to be ready' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n test get canary/podinfo | grep 'Initialized' && ok=true || ok=false + sleep 5 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n kuma-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +echo '✔ Canary initialization test passed' + +passed=$(kubectl -n test get svc/podinfo -o jsonpath='{.spec.selector.app}' 2>&1 | { grep podinfo-primary || true; }) +if [ -z "$passed" ]; then + echo -e '\u2716 podinfo selector test failed' + exit 1 +fi + +echo '✔ Canary service custom metadata test passed' + +echo '>>> Triggering canary deployment' +kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.1 + +echo '>>> Waiting for canary promotion' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n test describe deployment/podinfo-primary | grep '3.1.1' && ok=true || ok=false + sleep 10 + kubectl -n kuma-system logs deployment/flagger --tail 1 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n kuma-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +echo '>>> Waiting for canary finalization' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n test get canary/podinfo | grep 'Succeeded' && ok=true || ok=false + sleep 5 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n kuma-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +echo '✔ Canary promotion test passed' + +cat <>> Triggering canary deployment rollback test' +kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.2 + +echo '>>> Waiting for canary rollback' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n test get canary/podinfo | grep 'Failed' && ok=true || ok=false + sleep 10 + kubectl -n kuma-system logs deployment/flagger --tail 1 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n kuma-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +echo '✔ Canary rollback test passed' \ No newline at end of file diff --git a/test/workloads/init.sh b/test/workloads/init.sh index ff42fbe61..2b615a12e 100755 --- a/test/workloads/init.sh +++ b/test/workloads/init.sh @@ -13,6 +13,7 @@ echo '>>> Creating test namespace' kubectl create namespace test kubectl label namespace test istio-injection=enabled kubectl annotate namespace test linkerd.io/inject=enabled +kubectl annotate namespace test kuma.io/sidecar-injection=enabled echo '>>> Installing the load tester' kubectl apply -k ${REPO_ROOT}/kustomize/tester From 4ddc12185f05747ec3d3e4b28ce814d5f99361cf Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:18:56 -0800 Subject: [PATCH 06/11] Add prometheus support Signed-off-by: John Harris --- charts/flagger/templates/prometheus.yaml | 29 ++++++++++++++++++++++++ charts/flagger/templates/rbac.yaml | 13 +++++++++++ kustomize/base/prometheus/prometheus.yml | 29 ++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/charts/flagger/templates/prometheus.yaml b/charts/flagger/templates/prometheus.yaml index b15f50e15..f8bde4068 100644 --- a/charts/flagger/templates/prometheus.yaml +++ b/charts/flagger/templates/prometheus.yaml @@ -201,6 +201,35 @@ data: source_labels: - __meta_kubernetes_pod_name target_label: kubernetes_pod_name + + # scrape config for Kuma dataplanes + - job_name: 'kuma-dataplanes' + scrape_interval: "5s" + relabel_configs: + - source_labels: + - k8s_kuma_io_name + regex: "(.*)" + target_label: pod + - source_labels: + - k8s_kuma_io_namespace + regex: "(.*)" + target_label: namespace + - source_labels: + - __meta_kuma_mesh + regex: "(.*)" + target_label: mesh + - source_labels: + - __meta_kuma_dataplane + regex: "(.*)" + target_label: dataplane + - source_labels: + - __meta_kuma_service + regex: "(.*)" + target_label: service + - action: labelmap + regex: __meta_kuma_label_(.+) + kuma_sd_configs: + - server: http://kuma-control-plane.kuma-system:5676 --- apiVersion: apps/v1 kind: Deployment diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml index 8f27bbf8b..303aae156 100644 --- a/charts/flagger/templates/rbac.yaml +++ b/charts/flagger/templates/rbac.yaml @@ -195,6 +195,19 @@ rules: - update - patch - delete + - apiGroups: + - kuma.io + resources: + - trafficroutes + - trafficroutes/finalizers + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - nonResourceURLs: - /version verbs: diff --git a/kustomize/base/prometheus/prometheus.yml b/kustomize/base/prometheus/prometheus.yml index a73b09e75..aeba57b54 100644 --- a/kustomize/base/prometheus/prometheus.yml +++ b/kustomize/base/prometheus/prometheus.yml @@ -132,3 +132,32 @@ scrape_configs: source_labels: - __meta_kubernetes_pod_name target_label: kubernetes_pod_name + +# scrape config for Kuma dataplanes +- job_name: 'kuma-dataplanes' + scrape_interval: "5s" + relabel_configs: + - source_labels: + - k8s_kuma_io_name + regex: "(.*)" + target_label: pod + - source_labels: + - k8s_kuma_io_namespace + regex: "(.*)" + target_label: namespace + - source_labels: + - __meta_kuma_mesh + regex: "(.*)" + target_label: mesh + - source_labels: + - __meta_kuma_dataplane + regex: "(.*)" + target_label: dataplane + - source_labels: + - __meta_kuma_service + regex: "(.*)" + target_label: service + - action: labelmap + regex: __meta_kuma_label_(.+) + kuma_sd_configs: + - server: http://kuma-control-plane.kuma-system:5676 \ No newline at end of file From ae0f20a445927f8864b15fc71e711d4ccb5d89a7 Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:45:23 -0800 Subject: [PATCH 07/11] Add Kuma docs Signed-off-by: John Harris --- .gitbook.yaml | 1 + artifacts/examples/kuma-canary.yaml | 50 ++++ cmd/flagger/main.go | 2 +- docs/diagrams/flagger-kuma-canary.png | Bin 0 -> 123507 bytes .../tutorials/kuma-progressive-delivery.md | 252 ++++++++++++++++++ 5 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 artifacts/examples/kuma-canary.yaml create mode 100644 docs/diagrams/flagger-kuma-canary.png create mode 100644 docs/gitbook/tutorials/kuma-progressive-delivery.md diff --git a/.gitbook.yaml b/.gitbook.yaml index 1c8ec9fcf..725a74588 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -14,3 +14,4 @@ redirects: usage/crossover-progressive-delivery: tutorials/crossover-progressive-delivery.md usage/traefik-progressive-delivery: tutorials/traefik-progressive-delivery.md usage/osm-progressive-delivery: tutorials/osm-progressive-delivery.md + usage/kuma-progressive-delivery: tutorials/kuma-progressive-delivery.md diff --git a/artifacts/examples/kuma-canary.yaml b/artifacts/examples/kuma-canary.yaml new file mode 100644 index 000000000..443c09984 --- /dev/null +++ b/artifacts/examples/kuma-canary.yaml @@ -0,0 +1,50 @@ +apiVersion: flagger.app/v1beta1 +kind: Canary +metadata: + name: podinfo + namespace: test + annotations: + kuma.io/mesh: default +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + progressDeadlineSeconds: 60 + service: + port: 9898 + targetPort: 9898 + apex: + annotations: + 9898.service.kuma.io/protocol: "http" + canary: + annotations: + 9898.service.kuma.io/protocol: "http" + primary: + annotations: + 9898.service.kuma.io/protocol: "http" + analysis: + interval: 15s + threshold: 15 + maxWeight: 50 + stepWeight: 10 + metrics: + - name: request-success-rate + threshold: 99 + interval: 1m + - name: request-duration + threshold: 500 + interval: 30s + webhooks: + - name: acceptance-test + type: pre-rollout + url: http://flagger-loadtester.test/ + timeout: 30s + metadata: + type: bash + cmd: "curl -sd 'test' http://podinfo-canary.test:9898/token | grep token" + - name: load-test + type: rollout + url: http://flagger-loadtester.test/ + metadata: + cmd: "hey -z 2m -q 10 -c 2 http://podinfo-canary.test:9898/" \ No newline at end of file diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index e26121db1..8efb4cf87 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -106,7 +106,7 @@ func init() { flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.") flag.StringVar(&zapEncoding, "zap-encoding", "json", "Zap logger encoding.") flag.StringVar(&namespace, "namespace", "", "Namespace that flagger would watch canary object.") - flag.StringVar(&meshProvider, "mesh-provider", "istio", "Service mesh provider, can be istio, linkerd, appmesh, contour, gloo, nginx, skipper, traefik or osm.") + flag.StringVar(&meshProvider, "mesh-provider", "istio", "Service mesh provider, can be istio, linkerd, appmesh, contour, gloo, nginx, skipper, traefik, osm or kuma.") flag.StringVar(&selectorLabels, "selector-labels", "app,name,app.kubernetes.io/name", "List of pod labels that Flagger uses to create pod selectors.") flag.StringVar(&ingressAnnotationsPrefix, "ingress-annotations-prefix", "nginx.ingress.kubernetes.io", "Annotations prefix for NGINX ingresses.") flag.StringVar(&ingressClass, "ingress-class", "", "Ingress class used for annotating HTTPProxy objects.") diff --git a/docs/diagrams/flagger-kuma-canary.png b/docs/diagrams/flagger-kuma-canary.png new file mode 100644 index 0000000000000000000000000000000000000000..beae54c1b90bc1ffea9cabc503e3d16d3b441382 GIT binary patch literal 123507 zcmeFZg;$ha)IN-@D58YYs5D5If`Ecbhja)?cSyrHV1a@P(jWpN-O|zu(j_r;h={}x zl0yyr_6?8E`@CPQ_5A@K%e4e$X3p7XpS`br?Q7ruDoWBsgj9qC1O!B~GPhL;2u=zS z5FCj(c^v+Q&gn50{yOR`DXV@Gz8;-4eMvxYi9q)DEp?Cgb0a>sWbSc$n_6jfIPaLE zSO=%}k~gBqk6umX;XPDv=|^K@+!4j20x{P)b85_q$k2ol5Iy^y>yQGfuGFzyXQA0yC@3G6WB@Sa%oUqovUyli$PlQta{R#fQnfy*;;n08n zfndYP_{e{LPSaMf{m+jC~zFhq(GZ^aMug3v(-w>1i{Zh$`>i_#a z@;CbhQtJQwe6!B@|NroPd;EVky8`74V%n~8sTW;CFBcd}i4J2F!>@K*8;oXY*l60!;1v%yKhb`&|KH2S{^Tm`?!#5+ZMV`B z2k!8o)TLBfe$k;>M)RufCl#&SHMtOmdxr?3XyDtFJx=fJO8vH$n6;8Uyn`9VAzD#4 zb!uUIcHB8$sdF3zH_fhS-emjhTI#P&8UiyC6Qw#W)j2t{L+a@Q4-v4xT3dA1(n=K( z5$UO@&=GZ?XUBcwX*Ty zWMLr!1*xwO!gn|7F6w&usFl8W%Jy3O5J3vJ@iDTe0SCA1K^I~XO3aYpu_PL#IML_VwU;B{n2l8Y4?iA$$o29Sb zc#4T6;E{SRnczfxNbOACVS?7H-v}Fh|GCl+x@h1-yZW1H2)?xbSqNqbNs29Z3-@G9 zbNNO{z{7u@<5J|!(h~<4);titR{I#Q5alu{1YR1tQ7duZyOg5*AftfHDPQj%TjZc za6Opd&yQJ<6B>9){=-7^-v#*Tm%Gh|DvPt>cnAp0x-gjZzQLijw(sZs2|bXLihiSg zXyC7H!rp6WdVTQu(8_R#AWS|f?_NmHSfAGK;148VMuL~*Z;7&5_c!(>=`(C3q|owf z&HkAxC8(qgOWpK^cO;PcM);@x?_$}eqlpOKs#X?6j*h7>7=1X5yb+`EF^|N5jLupL zZadpHR*|0B=hY%cMsSm|67}n)fsKQ5ZtDp!JOKfm(eB@fD3=2sK60^)jgyFg|Nd2> z55FhdCh3l|pMzt%c}4TFgzmll1wIZ*v~aTr2QrCK9VST0eDF!m?>xAiiqT7QCk9Od z9<_5EUrzrMSHC1tP*Sqt&anrEe{hs27(P+dS z8v&5*wxJ z`YF;~QXwFFnm~;RT@Xx^S3?1A#ZR7hc<1k{!@{hX*M2*ULbNMGqoSqxukUD5M4PMn ze@pX|&RiYc)D!XKO5cr|dH9?+==L*feX`~#`nqY_inSz52Rfc#*2uG(At7`; z>383Wp5>b=nZG$0tc=jKXHtq~bqUhcSKIt98vByLiZ6jPupgw^$00Eb=z@aqU?s-8 zd+(Ddnrtv;LcyPm12a8^>?M7D`pD%0NXFe4{{I^mW% z1GVw7gU_M=C!IeIc^3$ulYJ=w&zAo6vrY)3Z=`F>)9YGhawez92v}LR%xmEkz*V)8 z^QWTQ-|mN`6!|gRYLYHh_VYY0@**g-dgT~1Gn1^*=G@Bj9AajSM!e%H#w`fLb9fbV zT*d8Rk6m!rc*4PpiOCPp!ObSc$*O??FJpPR$YAes=i7ZJSh!`!nMi~fWtGJRL@U|V zKF$d|ZkF8lh$tGkS}BL&CjNyoQDkb_4sPO^Tqh<{Mi*#0E#g_2Q6AUt)c7%2-5RhW z2!C8P(CDXa(Wee-{t7_}Y@ZSL&%}XJ+nocO_5KDJjAAJA6pW z@BH^HodTLMYD&t^lCiOb9B%2gg+2a{67P3~@p(9jW`V9bzr6-Kd`sPP``xAmXzI2R-fAxM@ntKpUXO@opbnfnKZnGGT z`blHoW$<$q|Nm`Uxv{yOEn|88iw+vUGUO%V(Q)5lXJn_Sy&TPM4r!{V=ZHjis_BJD zCJ zc9WL=-5S5aVoVyFF>5dd#-BUcXJg*6sl$qibJm7vY;-WI`TY&R1{u}4+?BLewpoow z>3Pntw%rx_M!1Va2QoO9TJ?+jN1~z%`7EZ|Q%!X;EV8}$a9Zk^`j~sD;oY(-Axx$y z8KDf-Jx1FwTc(sSUBxZ$7wK@V>@>{HYig)SWnXno&(8KdIdZ~fw#RA=-J7GIh~8dL zPfm6vkrpcTUTba<@&2hM+vT?=(YwMu$GP%Sg&ktu?DVvX02QYGP0T+>vU83jOp`}D zS;BkCH*TO(G#4LvRp*MVbrflDM7(`$SLpeP+J|^sZ13v2MAhzA*zS#}!T%K;*|Qyr zwLTh~n%*`%OF3I1@-cKOBt%Z@?qJK#*5l(7DK+kaZ*2109h7xjbBU*ydf#5?i>B;r z;yFxsTripRLO7#1jnd`=I6F6d)DRKVuZEk=jYX&uKX01lJfMiSHF;1_C!58`%(QfNC*F> zrG*(WRI}Jo*-`(1{`e3Pl)e{sVq8{HrC*<3A!4$^4bQ6ve-P(m==`92k&-f#Pt8!d z&Gb(4=0I=uF_v>mu_D>k{&!usS}8HTJrS0sp&jgNfzd_)Gq=XkxK3ee%9rH%44P?= zgzXjZ&?3JxWEWoMgU^rJQz{xC(M1<<|2a)Ah1Z3TZ=mJIl^aQ=G zmc1LLW=WN+4GL>$3+sX5p3h#@oQoc#i!#%u*V0+&-(|18k|rng?HW6MJTqlZu2pU+ zQ_G+|s>gBq$1S^25=CY0m&6T#9o~xX;7@p^&qvPS?T0tyB!mm(-CGpAH%W2 zsQ#Of0{+%fOd4$Hf)Ng$7xuD?vD`i%YqC{I4uJ)c%a=5uTkY{J_|unf{VeRQF~WTX zho$Tkv#o8c*LF^$iY8O_)`UAPMIJiAVY|AzDmAJ0c^jbD`n>j`Dn-=Mg8*7{jzb}? zp;#hf#eI3U$e;nOD>s5I*>S-Z%J3!fd1_Py2naYSM0l3F(b}=U$RGGtk&K+Efp|(=D1G z%dLGeCR6POb_Wa_v$A_Fcsm%sYvo|E*7<^VMwYUTLzBI$U%O}ad~fCF?VamHk7(6u z-#fOiY}649^iKq%W@o$NI~Uh-a+HWxiGNiD9)vn&SSTBf{r3#P%+pok>mJ`k484j z*~trkll@9kgi=XUfiD?1^!)DKBo$W1+LH>I&+n9ycb)cxyR{$QRAF_!Xdj4nfe{z& zMco%vhIa8)q$lYtb>Ho7O?L*7S=sjRm%C$o(z|E+PE-Y6C%GmaL;YNa4skG5fB zcYBfFU<{7fK`#Jy1+iyfLNxs$c&*m<`a+Ay48uWqx?aEFHvQvkenf42J2;BOemHMt zi4P!(0MpJppgJG!p|dMTaB)JN+nwdoftG!0%CaFySe|H5o-Un=TN5+LLHUR;Q?3ja z*{lZTR>z$m`#k2JH>KK3!75JU{*7UM;|qt!b}q^tj>Neq7>{Cx`6%KZ#?P2upFw zrmd+;vAstW<@tBxMzW6$Yh(?JZ+)lK`aE$~9=GZ2^}#BA(bQKhRS9dMSCWyL$&d9` zwXV5cj9r0JGLAQRyRa5qzUFZbSDZ!$8iBMw%k3`eCUpUC_d0ABh zA?)VIRv!%atbJ|LleNq)wDr6cWnibHCYS*Q55V!Kw`zk4PWnn(2QXsDc*A6pNRIV> z9s_U(FVIi$MDqYlO)r~WW>QFbcH3CJB_@`5O&KzgV5XMM*eH5}{n=cx`I>dZk!{bF z-`2H$M3A@J(>`62^&x$O1TP(})d8)&0SGL;6`s!BQ=*SHh{iP1Rm~6Ajx=A1F|1s6 zs_*XuCnduAT$M$xlyry@c&^#~4zM{b#|>xNS~*4<3GXY553dOS1CJ$>*^`W)g=GwW za+`m*ivPypZxFEE33j>OZbsXIzRKphSnV|V>Zy2Sxz|%X&A8(;WpOSDrGIlX?V6Vt@jjRI83R8g^3 zsK0P)|2Q7c%y&)F$xdR|sLp66haLjx)fsoMa%~CcDfXv?geZhiiR5!A*?M@ygwZ=$ zq7gYJPLbTe>0hNap%;3i2sf_=e@Z}7u%co+cG~;es~uAtQX?_Tpu363aJ6m{jyR6% zl}5E&ubWRT5S}Zr=xD#T3-`RxVN6`Q+5%_M)Ej8r6tdkV8wm%M63PzH+j}*UezW)^ zq-?=d@4yM|b=6MIt`7X>NPLTa+)!pkiI)HDqh{4ao^_wlLr7U7$>gXkLZNJP1&6Ok z)3f#B8@W3d?Dfvk2U-+9b&BuCmyS$1jV07Bx2F2KyUqt8wKWQ{<_iP0m%X zYb3c0;0@}#KHuQfaAUn=DsaF_U!D!uVaLv(`S{|$u|p5I9Uc1o zQu=^3@0iBzTli-jM+UbYi`K>OXM;In-J_=;t!a|Y2nVhm8eStp#Gf4A2fLk(?>&#R+dTq7LWc&NqXMsM-SvZL@bouYLm>6&T zhQD&Rp}VWA2npNvkin{gIwuf)K8|QVpkWFEiG52UN&D=4{1jdj1px0QIs3f?a%#qO zBc*;ED!GatKY`7)7~Zx4Y!j3#x|r;ZiK>9ON2zgshg|@w@A5=UPr6zv27`$nbt?(| zV?)Q6}A^9$g7QZHJz=ScRryr(SWRpGa=D zF^hNPbr>CfO-Szc>cUkQMv}Ai-X?d~ins*-jXH!csF`=@Q2C`{I0oD|30&SB=~>3f z3Y^|BuOEu!@lc?CIqeWYk>qMO<>QVeF@LGF4`e)4(8;ZBad>Mi7#w3o{|j= zkG_D?IEh=zXZku!{3lzy*PCvnO8+8?D|xS=-FatYDbK{V|Lqz@2;Y`Sxt6{0&H?xZ z0*k%HgI(zJn2EAbx7$x2nwT`FIP&oS|E)?DcudUNjpbzc;GNKR(+w zpa)QzL3(4!+2Z~9@3@KwQ*jhLaTz=dOOwvTE188q0zHwt>Z-61ufx@yYU$dwS7rebk`jYFdMyR22tz zc3huDZcql+_{vC$2#HY7=Qb7nHSLhgP>v79o3nX?P_w~WU@XO=h6570l{Mp zx0armSX*$o1}^X#R9bFX%N0(Oa$8tx;i@j@4l%v&6!}c{`Qbegs+fy&Oxfif@y?85 z!*;C0W@!Z)wvH^0%bs5r!!Lb`AIr^n9*g&qOTYcqWkyHe9z47UniUSZj5qGx|3>(Y zFx>J6IdwkUB%XB$%}$ZktlcK@J$@{S2xXJ{h}lFYz3I57zShl+>Dsf> zg3Ru|C*6lQN*n&+s04wMC=_opYVi*d zCz3Q)GWpuIv|_#ViDmUKlKLn)J}>;t$#5+=+`#Pm%E!E93Rklg#Su?XgdT#s Q2-va)FpS zZZH$gIX-FNc`fe5x*wl2iP`awvFHrz)j{zU6SWTos>3deCr-@>u8>(M+^*4OV{ zRF3qILa3a7c=Ug%oDdpf&K9F-&DqP|30cp-u6<3SyHEMOm|P9EPJAMRnyHhixeYk}roPfZ#XjsM={ z?90M-6|AT$SS>d$+OX!vib=WL)t|OoC61JhWwY&F5HP@g0Ch)sOUEVM-MvB{i(2sv zeC$k{xa^~)W#MHDn_1W$Rb3At+RS0D>oL+`@c%E?C)HCU<}ODhds8q`^CWJQ8Slzkl|3(fLUNiOk~U9DjOME}bmnq#(B z%c0~3r@9}Z%uzEz(ITEoWu7Y$2y*xnI~t@}uW9gG=`64vn7#|e`FRrNv-q=W!Va?? zOxdyiB>OD=wff~{iLow=_=|CAc`=PeQr2q&DT*=OLod$KU7dC?9;-qJ;)X(Z*TaJl z5V4|#^53n5B@F#Ad9okeH)Gl{Z+}c<_b$WC8oy&M%V9ki1@k&J6 zc6Mkx?JF9wt>6gZi*EEBnl!v>>_P~)^H0y>W|Iamn+1I=qZ0$Vr#3xo8VlYRCb?Xn zd8BzmHe|3HVj|2;GR2>Lo%6SP{ZQid z0(2f!4s#E&R}fJPC2=sHIddkA>3ntde4R_k&yu~>z&IbH+(aT@!~Lxe$8XNf@axd` z8$zE7P)zrb-8RnR{;MasCJO4T43JKV^PWlo8Ov5^Tuaxtn1_}$Okb3SQr^5$+M8eC zS3gbkAEv)r0D~fPSAz!qd{bF3wHz!5H1^@pV~L`9j4*j%AdJQ zfgF0Rp+V6H!Nxosbr5YHY#}(ZE{l%J!OBcH)37)`MtDV3%cphqDof>`g$JW1O}(cd zO-DeVfk&tzL%pu^NX?CfkS zipG1cDF!a5Dl}66u|gA9ZqucKM;qJ32%r>!ss}i&8~8sy^Misj=sgb%BrJ2 z4(k*XfLpGcoN-qeFIgU?yQtkzq!rg+;$Ubn!Qnc@?tZnavva&)DrzbYC4}c6f}(Y6 z&0pzHQ>t;T%l~51%-+e;io3`x(p~49nJUg*hNd>~e(iGKWcnd+V5-K50lNWKSd$T$ z9=JG!zKWa8enjsD5L$hxn99yNV_CmbU5iufbG}DrY#A4`>AUXC)>Ts|y`Pzs&nHJN zdl^2uwxC_ddR=Q$FjsqKBTm$Fe7)7)Vg@TI&yLb5e2`%Bvp!A?MRSU-+No-XFy@z_Wq*z3VoWM);s^L%mS09i*~f>rtDTm($@D(O<=_%@nP>L9e%P zSnAd>q??tdzNv1Dkda8*9Dc?)LvQFKW-kG{H3_=b1b-=})J zdV8(fU!-K^jbcD&F?$R|_=dIS3$J9N`v%3IBvL?njOHSz{=ed~Qcc~9G5D_u7}2fY zZ9i$YzlI;esJ}7i4ARx&w9~tX)F|1mEz5@Y*e1QFDWR#rNVVhKVas3<-D}5@;moW^ z$m=5C&1}RwFr3^=Oz$m4EtJ(!cDMl8Hyh1dw=83q+{1QOR&7xPKf*5+hH$T&K;B8T z^xsCK8Z@;ASp-c*rE`A0-+qTz6nGb`f@~QfmU8ddoeH5XcN1mSw*^XxKl+#J`$t-@ zuSo1uo5zfO`8yo}Cm|4xd|nEldfo)3I6pHH znUljx058Jq$8C$_h6@6dc6W;}q8IlTnqFNs|uE;5>oRrSv5c*s) z&-o>V4{4KJKec}qSxe=nW5g(gUD1v|iTfFhq=085JRVEDr^r7eLq&1-=D29GqN3`A zzTZNnmRB{yp2mka?w%|cuNX-|j|y;^e1eKO$u}DsSB$LKXTA?OQDy_Wt*r;D&mO9| zV%ELK)CwHjuJ8P*+I9g_x`D72m=qf&JPpFrQw!N51^e-;z?tT7j=)nV-3&5s#*Y>> z1S|A|H+caiF!*V=0boAaE23#6t}#61aTvW1#HC?j$5dagDAk%lHru39J5r+oAeQkd zMxYpwIYrJ>fvZLNKK5+k=d7%(6dn3{don63^<#wWl&XuZx___o){Gx_HJonuq*0(j z7o^62?f)=ayGAO0cE7OBAEYKX2AAI_uEsdl@QqXsmvN1cKAXG-gpm$?2-j~E4a$$+ zoiCZ|FB(F#zOWRS^+Xrcf!PD^KyM&6@XotPVU7#be=2$*Ik_k}vfZsazWtmyvn`r@ zNJk0AEODhfBH{mQ(-b39aLWpWDNXUNaFw z9Wh(EQqoh#>a3AubhL*oc-AJf=+D9jXlJ3M_hvTNHYMV8O0E_yJ3Wk@*?b@{bWQi7 zuM6M%t8$Cjszn9CJM|%KUmsweYkg=V1F{Jz0rL89*3Y(s)taMkL<>9Q?0)YYFC}z= zE{2HLDuZQ?p$v>O#I&?`(9@L=;-Pw~f7lL~$XxNmua$BW#i~z+^wl<#!a9k` z$r?~JyWGdw_MsrM-icj(LT2NOg)KGTAR+4pVM++|x-%05gs8J;X>X)SmpF8kDW@+; zuOD?!z%?RwKg)wj>-9Ls{)*P{4fYlx+6%Fd;#WbUVE_y`1uE-GR^5GWjce`lx2mSx#NRfA;2=rmvKvp6dA%+R#Xq zyq_|>Z*d{n2dVlNOZ+K=-?JtoMCU89NE)W70-bWVH^|1iE!sVltUnbN7S3Y7<~pyp zj{?%*TED+67E!6-mM}b-9lx<@B902wfkfK72;Mo1(elZbIzenc(ouHT+n)?uX0iQf z<#!tfnBBE@MEk>et+yQdHE4n^eoD8@0$yrCU#JzWsu4Jv6DKS}tu-RPBvBWfeXzd!o{P zHzM)T8h2b;XM|b%g!jhg%iZykT3;VWXy2>)lVU}p%$@Esa?4E~lxULwA05K?GeMdMFol@+wMRadHIEO_U&3#COhU4a-LQ4KW7d~?xP zFlAJrT9nJ4em`y9V|-u!HDCavQR1(sWeIf}VgvoRBy)7wf}YfL3ZuL-i2J~XZ@Er6 zC|{M`RCK(gNgQKEm`8dkI||b%X=#0{!>0jkbQ;AEY%o zd%l+DGOZ(LgEVJ;l`g(=ntPXVuarLZms-XqK2Y81htHnjo=rV2i?0(RG?T`KQBFJS zWiIX!;K_j5$z2((h~C;sP_Z83ISl;6H4fEWVrIb`y7SkBm{Jb?QnehjRp8Xb_}PY1 zGcFz+FRQ|}IsMCK?2gLTP_wHYInahjY12*ll9 zyScgc3+C7ald`kW5MyLUxJ;;}MyVB4eIzE32dB{X742&b2G5Ru34|hjy<38LsoAxJv53)-{UO|})@K6fstwE>3=S z5v!JzmuDLqtug>Pr>Cn2l_HHP!=YWdF8A9qbC7tNJDT?!#qH?ud3t0s+5 zn2+ycj4;d8{a{CH*I#fAbOj|e{K`_S)xUnY+*E$T4fHU+?((YUHCIT49(4eg=pmEM zKqNnJi?h>u9}KHcVS1)2#jjm&dWC%^3p5%l*%hNhmn0Ud2BN%|WyJ?r1hnHu3sS>s zm+xe$=z@}qa59h_#2!h>IN?dLuj9~);;gJr@2R~_c}{Hq$~-=^HtuCN{5kyxhSwnL zg&btbItpFQ^{QUzR7FbFKlf6Y>H>$9{`l49>6P&JY57ddP7+=^g4OJa!v*0RY3WpJ zVt%fHDy$z=WPt5Q2Jm0KzUJs`pRPoTDjL2XvZlEcX9)~|&}O7bNrh;W?>%NC z`OFNI(BBBf5rGn;cb;SwDGlU!srS5!4=)~c>8l9!X(!AztUt-!S8^Mht6G#RA?f5f ztScn^!OZztvDRr+k1@p|2+3=|qzW*uyunIYH=sIz5{vxI?`w}6mTG`8znJdb8=cAU zLOQdDo|uxJ!V7tg_(HMn0}Bg}iQc;v$D>%=b&l%oG)yF~UseIXq}V z%JW4bghR7&YA;c>$jIxjz`n7`ixm{Gk?yDf&~vAb^+8e9^^2J!E3Su>v%q%ki+4ql z085ZXcZ2oTlSRv<@f

wb#9P@_7|oh*R4t*zZM`WeHCo#1t1(_6O9T;=42woR341!-Xo$L@O6i;s>q3+^xai78j)a(+Z-v_Nu(k*e&it=gdzBA`DnM^GD*%^28CFnD8 z?PFlabUW13Z#IG^eRa=!xw*1v3ZR#Qi;WfsJgz{yz0!kY7&qN=*4CvvY2A{qQvVRz zGU)5S6l`VEkKW~pQr9mE=1PursHx#{wVRI7w<;?W!Jv2Vdu?xCT#tAt5Y^hvG zE|dz*>4|sWFP-Q3e4gXO{g)S2PBBw)^s`)30eAbWz-(+X=9Fnr;sJe+I4s_l>nlk02a!9(N>(e>TdcxNkZh_fE%IKhx?;MJ8zHgdb`6WtqdS=aX<=QOZ$AqAjc$4Lcb?vchPJe7 zEyO=T3HQV1prL`Z&_3M)yb7&YW##Ks8XTnLjNG>S8Uf7*w0tjMswhXeAN%0%s2MAT%X$*Xs47HNqkg8YyJ^>I%3srs_}Ek`2lUStof3zcy0GS=%+1uNz`By=) zO!b;*cF$`OJwl}<_QbHryr>t?jaA1-VsyqXZu^*&2|kjC46C_OjTaunQ%$vD40G?N zmoJuOrcJoT2x8B+y%WTC7otvAqt-3LgA<5Iim{^gOTmP0rDLBV^}$ zzyUO%Y#nj?2re<%)<$W^?9`-zNr1?SWHlc;-JGrt-)Zm%?8G7hSk0V|%s6w#P^*j` ziN>b_!FPJ`todq7d$Gte@E~x_0m`I|Sx_9}%%R5YY~8F!L4)egRSrPj5vIp}Bd)(y z*DO*|6Y#{ISi1ePQZ3jt6I!o{}!rv`T`$v+(17c6;Cl^EoUdZjy2uR{b!b z@HLc5Gg~KiZ5wVUOphUi{sf&(6(oU<6TdTmYt00w`OHi$A$JAvRG_oHb%rW%=F&P? zn5D>mXBfV;ThJ%H21vETWIH_XEC{hYAIueikI?z2c$t;A$OXNAowhRja`AdSHrNuT z8MLm;nZ=m%C4Ljm7lq=yzSg+}CWtEb+=hv#8i4+5^vnWdxvuFtwBwPXE(~U7vl8ki z%zBr+XL#HqOsWZ=t&BtcS*||25K`YOHgN8<6A(ws*q4VlV;!ZJjaBh+nSP%wv_7bee|H>;W;Ts&mC3L`6J z)>cCSvsviu(-?aB?BW|s$pscA2#v^#Nd2%8$HY)NG3Kgy5=;QtP$c6(;#%ZdFh9_2 z%T;BxO_G-b3T!)|5xTXp8+=9Y3Zi;BPp6>F^rjf@@ro~0`@>{U9R7JZx68KpT*>0Z znxi;KNB?{VdiQ$Zq;L+HAvTj{>N$po<$Yc}nmlpjMfKj7yb&IjEK7lRyB+9uC?816 z&MwGm_f|hKKqN^Wzl*j=eG?soDiHkeg?{PqQjLPte~$k+%;PmIIohv8Sbim^s;^~F z5Y>UhKK9Hh%s!8S@Ac{YerjH6$=SoZ@DRaC#jNH3kxhNUo#J=Ohr&);jbHA zcXD2xg5yvA6Kj$Sd8JuYc+|}DVQln0)j@&KEvLrsdi2B>i?Uo8Z4tqGM4Kx!T1H*r zwMNRo^7Q&VrP&)%5s_ss8=5zFi-1L{A?_VX4ik3p7m+j3t7sOlhWRLl^UiC!xfA?l zq=%3JLF82pZt(@%JEb4>Ux55KZtBeOOqYbA@Y}k1e7Vk7N)Wa23*1QLJ$w2~5EO2@ z?H@Y;&mEP6!MMkiUvlMYO%<%)s^ag>6SJ|rLalu)66rG7^8A?fysndO5ffkDca+vF ziU~BDa?mRFWFpB5k6Ngjhw@+GFqjER=1CYi@ool;E7YE81crw(k9T0`) zS?VoCTPh)g>x)|Q^4RC0WUzRClEc8|#AQwae=IifD5m5#?GTi@f%6W2e)$WF?n=3s zaD-O2@$S1Ezue{j&9dxHkgvf&9by4wKJcIUFWAa?ZjVKH!B5|Gm}>i*l%pO3u|`Sk zomv3($SMetsix^wpz?zcDtb7PA}hLQ3Iy+pE~ON4pX@z{&0TW)%+9AXa*I0*1nQ{Em6>k!uN zaQMuHUpsj`v*(bNpvTCpa6x)#VsdQ1 zet!JNKQD>?JoeK!`Z??*ZG68>inMgxQH?4Q-)u_Xf!QvX7vxDsdp-8LWO9p7|gc#w< z7KM&n;$k04e19;>aEF_XQx?X!kYB&0r(s1xP8!*9gHl;a!l-O=!UvYdKLZ%wULS6Ev*m^k0R-^r&nX6z$s=aT zBTYmKXZD9}zQTatfjj;u3h|ggS_L{a97PfO6UtfXA%93up}lT3Ev7g)E^NX`8EaBZ zOHzlpGTHon7!6?QkQH_q?V8BG44mX^@~HIuZrNZUW`cl^tb0d;fCzH+z_f@raO;}SI$;@)v(sr)7 z-eG2_5#>5Y(~w^d_1GdBuYJlN#Kv{TM|31CxHex`fAKv6vc-W+ z9)Rm71_PPP22t0M{^Hn4B-wm<%}Yx6T3s&ucnK8k;YIiteb%ihFB3q8R3@%Hd}0UB z%a5*rpP&d{!EtH0iCoIGl-X#vt&SP+ae%o92lqfS!5L%!=Wkvoo%RoC(vW$Ypgo-Y zZhp8jm*omDz|! zf8DL%afOiBk>y0Zp9vp3ZJ1laNGEFT58Yq{_F2=aoE&y|eI-bXH!wH|+RKpb*YC>){G@Qu`R&Bs8wIMNqRLg(UdB7n zr~BAx*=~AknsQL!uNI*ATRT?Ekg9^jX;9xa*)*uG0`X=Ssq3#Z>Cwh>I3qV4L1G9o`7x)yaLYixo#Z>9 zm5e1WDWpu7m)0`7E@vH{9L9cY1gCHP=#6`{?tKe(Q_hL2`LK9^*t*sIrB31GQFW6B zn^Vc(4f>MBmGUYcPP;a^!3N%GWvM6S@lL#Rado|bCeJYRwAywB4RK1A%c+C0FF5TW z52G^xGXB$%Z)Nq(=}9+TC?-7^$GRRHNt?jE5jLBjgLSdbu{xPPU88lm3x0X9_@i#ZIsX9FW){x}+%flMxNS~Sc^kkMV5AJT@W$h^uZPiZ7^tN8K41Z&o+4y3^fJ1J!Znk>W)Gta<|{}p zKxqT)aG9st9na}%dmR<*MUKjvuF<|h-I)P;{^SlOwly!v{G{jC_lkTvy6E{4CP%zs zt4_5q|0m;B$p6-hwLrI&bx_C;6Ir@^c)Me*%6Yg#a3dZ>QM+(Ey*f^DpHdbrKN(9-tcf^*u@CaR7NH z(qgF7%9_>Vs`=uXg6Ro>fMJqbV^u0%4nU4963Z-KEP`0A_xp2{r8Iyx2cl$BaxuKQ zf$ULflG^p^g zjakzC`4CsTAnwD3cLnzW4Q7MueN*Xw26X?)$fy1Xi1C;~cwbxXRz^kzX1_c+g6QWf zoqy5NUUR&uEP*eCLwGnRfsxlSC>u@;5}%Wa)ETK@%6LO99PX)J3WG`<$ywQT*h4${ zrMHUgGNZps?Lqq&Qne|?Ats>tD|E{+sW59Q;KJehCgw_dNcV(Ucvf+}eR>$Cpj$&) z`o&c3251MGHew#)9Jov;jhNP2t5S2?^YG(~5`TtsLB#+Q63TFLRtde&HFhI(<%uhmVh8yhZr41NtTtW`k%Y zmeM1x>U^{mKvZWi3{8T5H>`_Zd|j{NU$M5<%2sEvyrfj$S-B43gHd$W{yhz$Tm<$};6>er&# zVc=W~hJC!4jIIj(@UZ0a+#~X>C8FHkOizj%0DJ=XS}6$q0~>{mj|g%*D7{(PDb0$< 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 0.5s + Halt podinfo.test advancement request duration 1.45s > 0.5s + Rolling back podinfo.test failed checks threshold reached 5 + Canary failed! Scaling down podinfo.test +``` + +The above procedures can be extended with [custom metrics](../usage/metrics.md) checks, [webhooks](../usage/webhooks.md), [manual promotion](../usage/webhooks.md#manual-gating) approval and [Slack or MS Teams](../usage/alerting.md) notifications. From e8d7001f5e09d500a7eb58fcdb0d975af4f99863 Mon Sep 17 00:00:00 2001 From: John Harris Date: Sat, 18 Dec 2021 14:51:57 -0800 Subject: [PATCH 08/11] Add RO FS back to deployment Signed-off-by: John Harris --- kustomize/base/flagger/deployment.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/kustomize/base/flagger/deployment.yaml b/kustomize/base/flagger/deployment.yaml index af0c3f608..f0bd8b133 100644 --- a/kustomize/base/flagger/deployment.yaml +++ b/kustomize/base/flagger/deployment.yaml @@ -53,4 +53,5 @@ spec: memory: "32Mi" cpu: "10m" securityContext: + readOnlyRootFilesystem: true runAsUser: 10001 From caefaf73aa11ce8a0ce561b4b05f2665ad2bea31 Mon Sep 17 00:00:00 2001 From: John Harris Date: Mon, 20 Dec 2021 09:21:42 -0800 Subject: [PATCH 09/11] Add additional docs references Signed-off-by: John Harris --- docs/gitbook/README.md | 1 + docs/gitbook/SUMMARY.md | 1 + docs/gitbook/usage/deployment-strategies.md | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/gitbook/README.md b/docs/gitbook/README.md index d4946e5d8..e85e2c8e3 100644 --- a/docs/gitbook/README.md +++ b/docs/gitbook/README.md @@ -37,6 +37,7 @@ After installing Flagger, you can follow one of these tutorials to get started: * [Linkerd](tutorials/linkerd-progressive-delivery.md) * [AWS App Mesh](tutorials/appmesh-progressive-delivery.md) * [Open Service Mesh](tutorials/osm-progressive-delivery.md) +* [Kuma](tutorials/kuma-progressive-delivery.md) **Ingress controller tutorials** diff --git a/docs/gitbook/SUMMARY.md b/docs/gitbook/SUMMARY.md index 5439a16d0..ef8b4e06d 100644 --- a/docs/gitbook/SUMMARY.md +++ b/docs/gitbook/SUMMARY.md @@ -31,6 +31,7 @@ * [Skipper Canary Deployments](tutorials/skipper-progressive-delivery.md) * [Traefik Canary Deployments](tutorials/traefik-progressive-delivery.md) * [Open Service Mesh Deployments](tutorials/osm-progressive-delivery.md) +* [Kuma Canary Deployments](tutorials/kuma-progressive-delivery.md) * [Blue/Green Deployments](tutorials/kubernetes-blue-green.md) * [Canary analysis with Prometheus Operator](tutorials/prometheus-operator.md) * [Zero downtime deployments](tutorials/zero-downtime-deployments.md) diff --git a/docs/gitbook/usage/deployment-strategies.md b/docs/gitbook/usage/deployment-strategies.md index 9483dbb9e..7e5c1c5cf 100644 --- a/docs/gitbook/usage/deployment-strategies.md +++ b/docs/gitbook/usage/deployment-strategies.md @@ -3,7 +3,7 @@ Flagger can run automated application analysis, promotion and rollback for the following deployment strategies: * **Canary Release** \(progressive traffic shifting\) - * Istio, Linkerd, App Mesh, NGINX, Skipper, Contour, Gloo Edge, Traefik, Open Service Mesh + * Istio, Linkerd, App Mesh, NGINX, Skipper, Contour, Gloo Edge, Traefik, Open Service Mesh, Kuma * **A/B Testing** \(HTTP headers and cookies traffic routing\) * Istio, App Mesh, NGINX, Contour, Gloo Edge * **Blue/Green** \(traffic switching\) From cb3b5cba90a279936e37c49dbce9562025204ec0 Mon Sep 17 00:00:00 2001 From: John Harris Date: Wed, 5 Jan 2022 07:18:41 -0800 Subject: [PATCH 10/11] Remove Prometheus from default install Signed-off-by: John Harris --- charts/flagger/templates/prometheus.yaml | 29 ---------------------- kustomize/base/prometheus/prometheus.yml | 31 +----------------------- 2 files changed, 1 insertion(+), 59 deletions(-) diff --git a/charts/flagger/templates/prometheus.yaml b/charts/flagger/templates/prometheus.yaml index f8bde4068..b15f50e15 100644 --- a/charts/flagger/templates/prometheus.yaml +++ b/charts/flagger/templates/prometheus.yaml @@ -201,35 +201,6 @@ data: source_labels: - __meta_kubernetes_pod_name target_label: kubernetes_pod_name - - # scrape config for Kuma dataplanes - - job_name: 'kuma-dataplanes' - scrape_interval: "5s" - relabel_configs: - - source_labels: - - k8s_kuma_io_name - regex: "(.*)" - target_label: pod - - source_labels: - - k8s_kuma_io_namespace - regex: "(.*)" - target_label: namespace - - source_labels: - - __meta_kuma_mesh - regex: "(.*)" - target_label: mesh - - source_labels: - - __meta_kuma_dataplane - regex: "(.*)" - target_label: dataplane - - source_labels: - - __meta_kuma_service - regex: "(.*)" - target_label: service - - action: labelmap - regex: __meta_kuma_label_(.+) - kuma_sd_configs: - - server: http://kuma-control-plane.kuma-system:5676 --- apiVersion: apps/v1 kind: Deployment diff --git a/kustomize/base/prometheus/prometheus.yml b/kustomize/base/prometheus/prometheus.yml index aeba57b54..f302da1e0 100644 --- a/kustomize/base/prometheus/prometheus.yml +++ b/kustomize/base/prometheus/prometheus.yml @@ -131,33 +131,4 @@ scrape_configs: - action: replace source_labels: - __meta_kubernetes_pod_name - target_label: kubernetes_pod_name - -# scrape config for Kuma dataplanes -- job_name: 'kuma-dataplanes' - scrape_interval: "5s" - relabel_configs: - - source_labels: - - k8s_kuma_io_name - regex: "(.*)" - target_label: pod - - source_labels: - - k8s_kuma_io_namespace - regex: "(.*)" - target_label: namespace - - source_labels: - - __meta_kuma_mesh - regex: "(.*)" - target_label: mesh - - source_labels: - - __meta_kuma_dataplane - regex: "(.*)" - target_label: dataplane - - source_labels: - - __meta_kuma_service - regex: "(.*)" - target_label: service - - action: labelmap - regex: __meta_kuma_label_(.+) - kuma_sd_configs: - - server: http://kuma-control-plane.kuma-system:5676 \ No newline at end of file + target_label: kubernetes_pod_name \ No newline at end of file From d2038699c025aaa0e0327c6e46efd3dc25d28606 Mon Sep 17 00:00:00 2001 From: John Harris Date: Wed, 5 Jan 2022 07:46:56 -0800 Subject: [PATCH 11/11] Fix newlines Signed-off-by: John Harris --- artifacts/examples/kuma-canary.yaml | 2 +- kustomize/base/prometheus/prometheus.yml | 2 +- kustomize/kuma/patch.yaml | 2 +- test/kuma/run.sh | 2 +- test/kuma/test-canary.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/artifacts/examples/kuma-canary.yaml b/artifacts/examples/kuma-canary.yaml index 443c09984..12dbb416b 100644 --- a/artifacts/examples/kuma-canary.yaml +++ b/artifacts/examples/kuma-canary.yaml @@ -47,4 +47,4 @@ spec: type: rollout url: http://flagger-loadtester.test/ metadata: - cmd: "hey -z 2m -q 10 -c 2 http://podinfo-canary.test:9898/" \ No newline at end of file + cmd: "hey -z 2m -q 10 -c 2 http://podinfo-canary.test:9898/" diff --git a/kustomize/base/prometheus/prometheus.yml b/kustomize/base/prometheus/prometheus.yml index f302da1e0..a73b09e75 100644 --- a/kustomize/base/prometheus/prometheus.yml +++ b/kustomize/base/prometheus/prometheus.yml @@ -131,4 +131,4 @@ scrape_configs: - action: replace source_labels: - __meta_kubernetes_pod_name - target_label: kubernetes_pod_name \ No newline at end of file + target_label: kubernetes_pod_name diff --git a/kustomize/kuma/patch.yaml b/kustomize/kuma/patch.yaml index 78c084012..cbcc4f880 100644 --- a/kustomize/kuma/patch.yaml +++ b/kustomize/kuma/patch.yaml @@ -11,4 +11,4 @@ spec: - -log-level=info - -include-label-prefix=app.kubernetes.io - -mesh-provider=kuma - - -metrics-server=http://prometheus-server.kuma-metrics:80 \ No newline at end of file + - -metrics-server=http://prometheus-server.kuma-metrics:80 diff --git a/test/kuma/run.sh b/test/kuma/run.sh index fff250d6f..67153fa9f 100755 --- a/test/kuma/run.sh +++ b/test/kuma/run.sh @@ -8,4 +8,4 @@ DIR="$(cd "$(dirname "$0")" && pwd)" "$DIR"/install.sh "$REPO_ROOT"/test/workloads/init.sh -"$DIR"/test-canary.sh \ No newline at end of file +"$DIR"/test-canary.sh diff --git a/test/kuma/test-canary.sh b/test/kuma/test-canary.sh index 68ce67caa..5b86906bd 100755 --- a/test/kuma/test-canary.sh +++ b/test/kuma/test-canary.sh @@ -193,4 +193,4 @@ until ${ok}; do fi done -echo '✔ Canary rollback test passed' \ No newline at end of file +echo '✔ Canary rollback test passed'