diff --git a/Gopkg.lock b/Gopkg.lock index 748041c8..4ed709da 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,14 +25,6 @@ revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" -[[projects]] - digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45" - name = "github.com/fatih/color" - packages = ["."] - pruneopts = "NUT" - revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4" - version = "v1.7.0" - [[projects]] digest = "1:81466b4218bf6adddac2572a30ac733a9255919bc2f470b4827a317bd4ee1756" name = "github.com/ghodss/yaml" @@ -92,10 +84,11 @@ revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306" [[projects]] - digest = "1:2e3c336fc7fde5c984d2841455a658a6d626450b1754a854b3b32e7a8f49a07a" + digest = "1:d2754cafcab0d22c13541618a8029a70a8959eb3525ff201fe971637e2274cd0" name = "github.com/google/go-cmp" packages = [ "cmp", + "cmp/cmpopts", "cmp/internal/diff", "cmp/internal/function", "cmp/internal/value", @@ -191,22 +184,6 @@ pruneopts = "NUT" revision = "c15d7c8f2220a7578b33504df6edefa948c845ae" -[[projects]] - digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061" - name = "github.com/mattn/go-colorable" - packages = ["."] - pruneopts = "NUT" - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" - -[[projects]] - digest = "1:bffa444ca07c69c599ae5876bc18b25bfd5fa85b297ca10a25594d284a7e9c5d" - name = "github.com/mattn/go-isatty" - packages = ["."] - pruneopts = "NUT" - revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" - version = "v0.0.4" - [[projects]] digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" @@ -675,8 +652,8 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/fatih/color", "github.com/google/go-cmp/cmp", + "github.com/google/go-cmp/cmp/cmpopts", "github.com/istio/glog", "github.com/knative/pkg/apis/istio/v1alpha3", "github.com/knative/pkg/client/clientset/versioned", @@ -685,6 +662,7 @@ "go.uber.org/zap", "go.uber.org/zap/zapcore", "k8s.io/api/apps/v1", + "k8s.io/api/autoscaling/v1", "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", @@ -693,6 +671,7 @@ "k8s.io/apimachinery/pkg/runtime/schema", "k8s.io/apimachinery/pkg/runtime/serializer", "k8s.io/apimachinery/pkg/types", + "k8s.io/apimachinery/pkg/util/intstr", "k8s.io/apimachinery/pkg/util/runtime", "k8s.io/apimachinery/pkg/util/sets/types", "k8s.io/apimachinery/pkg/util/wait", diff --git a/Makefile b/Makefile index 4e078729..fbfbe2ab 100644 --- a/Makefile +++ b/Makefile @@ -68,3 +68,7 @@ release-set: fmt version-set helm-package git tag $(VERSION) git push origin $(VERSION) +reset-test: + kubectl delete -f ./artifacts/namespaces + kubectl apply -f ./artifacts/namespaces + kubectl apply -f ./artifacts/canaries diff --git a/artifacts/canaries/canary.yaml b/artifacts/canaries/canary.yaml new file mode 100644 index 00000000..1680b989 --- /dev/null +++ b/artifacts/canaries/canary.yaml @@ -0,0 +1,37 @@ +apiVersion: flagger.app/v1beta1 +kind: CanaryDeployment +metadata: + name: podinfo + namespace: test +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + service: + port: 9898 + gateways: + - public-gateway.istio-system.svc.cluster.local + hosts: + - app.istio.weavedx.com + canaryAnalysis: + # max number of failed metric checks + # before rolling back the canary + threshold: 5 + # max traffic percentage routed to canary + # percentage (0-100) + maxWeight: 50 + # canary increment step + # percentage (0-100) + stepWeight: 10 + metrics: + - name: istio_requests_total + # minimum req success rate (non 5xx responses) + # percentage (0-100) + threshold: 99 + interval: 1m + - name: istio_request_duration_seconds_bucket + # maximum req duration P99 + # milliseconds + threshold: 500 + interval: 30s diff --git a/artifacts/canaries/deployment.yaml b/artifacts/canaries/deployment.yaml new file mode 100644 index 00000000..895dadb1 --- /dev/null +++ b/artifacts/canaries/deployment.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo + namespace: test + labels: + app: podinfo +spec: + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: quay.io/stefanprodan/podinfo:1.2.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9898 + name: http + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: blue + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + failureThreshold: 3 + periodSeconds: 3 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index fd2ac3ef..cb1b1f67 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -64,4 +64,48 @@ spec: pattern: "^[0-9]+(m)" threshold: type: number - +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: canarydeployments.flagger.app +spec: + group: flagger.app + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + names: + plural: canarydeployments + singular: canarydeployment + kind: CanaryDeployment + scope: Namespaced + validation: + openAPIV3Schema: + properties: + spec: + required: + - canaryAnalysis + properties: + canaryAnalysis: + properties: + threshold: + type: number + maxWeight: + type: number + stepWeight: + type: number + metrics: + type: array + properties: + items: + type: object + properties: + name: + type: string + interval: + type: string + pattern: "^[0-9]+(m)" + threshold: + type: number diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index 6693cb33..6a5e13a2 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -5,6 +5,8 @@ import ( "log" "time" + "github.com/stefanprodan/flagger/pkg/operator" + _ "github.com/istio/glog" sharedclientset "github.com/knative/pkg/client/clientset/versioned" "github.com/knative/pkg/signals" @@ -71,6 +73,7 @@ func main() { rolloutInformerFactory := informers.NewSharedInformerFactory(rolloutClient, time.Second*30) rolloutInformer := rolloutInformerFactory.Flagger().V1beta1().Canaries() + canaryDeploymentInformer := rolloutInformerFactory.Flagger().V1beta1().CanaryDeployments() logger.Infof("Starting flagger version %s revision %s", version.VERSION, version.REVISION) @@ -101,11 +104,22 @@ func main() { logger, ) + cd := operator.NewController( + kubeClient, + sharedClient, + rolloutClient, + canaryDeploymentInformer, + controlLoopInterval, + metricsServer, + logger, + ) + rolloutInformerFactory.Start(stopCh) logger.Info("Waiting for informer caches to sync") for _, synced := range []cache.InformerSynced{ rolloutInformer.Informer().HasSynced, + canaryDeploymentInformer.Informer().HasSynced, } { if ok := cache.WaitForCacheSync(stopCh, synced); !ok { logger.Fatalf("Failed to wait for cache sync") @@ -119,5 +133,12 @@ func main() { } }(c) + // start controller + go func(ctrl *operator.Controller) { + if err := ctrl.Run(2, stopCh); err != nil { + logger.Fatalf("Error running controller: %v", err) + } + }(cd) + <-stopCh } diff --git a/pkg/apis/flagger/v1beta1/register.go b/pkg/apis/flagger/v1beta1/register.go index 26162590..a6d879be 100755 --- a/pkg/apis/flagger/v1beta1/register.go +++ b/pkg/apis/flagger/v1beta1/register.go @@ -47,6 +47,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Canary{}, &CanaryList{}, + &CanaryDeployment{}, + &CanaryDeploymentList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/flagger/v1beta1/types.go b/pkg/apis/flagger/v1beta1/types.go index 0e39f3b5..10826c8e 100755 --- a/pkg/apis/flagger/v1beta1/types.go +++ b/pkg/apis/flagger/v1beta1/types.go @@ -17,9 +17,12 @@ limitations under the License. package v1beta1 import ( + hpav1 "k8s.io/api/autoscaling/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const CanaryDeploymentKind = "CanaryDeployment" + // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -41,6 +44,23 @@ type CanarySpec struct { VirtualService VirtualService `json:"virtualService"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CanaryList is a list of Canary resources +type CanaryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []Canary `json:"items"` +} + +// CanaryStatus is used for state persistence (read-only) +type CanaryStatus struct { + State string `json:"state"` + CanaryRevision string `json:"canaryRevision"` + FailedChecks int `json:"failedChecks"` +} + type Target struct { Name string `json:"name"` Host string `json:"host"` @@ -63,19 +83,49 @@ type Metric struct { Threshold int `json:"threshold"` } -// CanaryStatus is the status for a Canary resource -type CanaryStatus struct { - State string `json:"state"` - CanaryRevision string `json:"canaryRevision"` - FailedChecks int `json:"failedChecks"` +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Canary is a specification for a Canary resource +type CanaryDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CanaryDeploymentSpec `json:"spec"` + Status CanaryDeploymentStatus `json:"status"` +} + +// CanarySpec is the spec for a Canary resource +type CanaryDeploymentSpec struct { + // reference to target resource + TargetRef hpav1.CrossVersionObjectReference `json:"targetRef"` + + // virtual service spec + Service CanaryDeploymentService `json:"service"` + + // metrics and thresholds + CanaryAnalysis CanaryAnalysis `json:"canaryAnalysis"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // CanaryList is a list of Canary resources -type CanaryList struct { +type CanaryDeploymentList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` - Items []Canary `json:"items"` + Items []CanaryDeployment `json:"items"` +} + +// CanaryStatus is used for state persistence (read-only) +type CanaryDeploymentStatus struct { + State string `json:"state"` + CanaryRevision string `json:"canaryRevision"` + FailedChecks int `json:"failedChecks"` +} + +type CanaryDeploymentService struct { + Port int32 `json:"port"` + Gateways []string `json:"gateways"` + Hosts []string `json:"hosts"` } diff --git a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go index 5cd11c54..a10d99f3 100644 --- a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go @@ -73,6 +73,128 @@ func (in *CanaryAnalysis) DeepCopy() *CanaryAnalysis { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryDeployment) DeepCopyInto(out *CanaryDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryDeployment. +func (in *CanaryDeployment) DeepCopy() *CanaryDeployment { + if in == nil { + return nil + } + out := new(CanaryDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CanaryDeployment) 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 *CanaryDeploymentList) DeepCopyInto(out *CanaryDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CanaryDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryDeploymentList. +func (in *CanaryDeploymentList) DeepCopy() *CanaryDeploymentList { + if in == nil { + return nil + } + out := new(CanaryDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CanaryDeploymentList) 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 *CanaryDeploymentService) DeepCopyInto(out *CanaryDeploymentService) { + *out = *in + if in.Gateways != nil { + in, out := &in.Gateways, &out.Gateways + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryDeploymentService. +func (in *CanaryDeploymentService) DeepCopy() *CanaryDeploymentService { + if in == nil { + return nil + } + out := new(CanaryDeploymentService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryDeploymentSpec) DeepCopyInto(out *CanaryDeploymentSpec) { + *out = *in + out.TargetRef = in.TargetRef + in.Service.DeepCopyInto(&out.Service) + in.CanaryAnalysis.DeepCopyInto(&out.CanaryAnalysis) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryDeploymentSpec. +func (in *CanaryDeploymentSpec) DeepCopy() *CanaryDeploymentSpec { + if in == nil { + return nil + } + out := new(CanaryDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryDeploymentStatus) DeepCopyInto(out *CanaryDeploymentStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryDeploymentStatus. +func (in *CanaryDeploymentStatus) DeepCopy() *CanaryDeploymentStatus { + if in == nil { + return nil + } + out := new(CanaryDeploymentStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CanaryList) DeepCopyInto(out *CanaryList) { *out = *in diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/canarydeployment.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/canarydeployment.go new file mode 100644 index 00000000..f26eae6c --- /dev/null +++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/canarydeployment.go @@ -0,0 +1,174 @@ +/* +Copyright The Flagger 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 v1beta1 + +import ( + v1beta1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + scheme "github.com/stefanprodan/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" +) + +// CanaryDeploymentsGetter has a method to return a CanaryDeploymentInterface. +// A group's client should implement this interface. +type CanaryDeploymentsGetter interface { + CanaryDeployments(namespace string) CanaryDeploymentInterface +} + +// CanaryDeploymentInterface has methods to work with CanaryDeployment resources. +type CanaryDeploymentInterface interface { + Create(*v1beta1.CanaryDeployment) (*v1beta1.CanaryDeployment, error) + Update(*v1beta1.CanaryDeployment) (*v1beta1.CanaryDeployment, error) + UpdateStatus(*v1beta1.CanaryDeployment) (*v1beta1.CanaryDeployment, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1beta1.CanaryDeployment, error) + List(opts v1.ListOptions) (*v1beta1.CanaryDeploymentList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.CanaryDeployment, err error) + CanaryDeploymentExpansion +} + +// canaryDeployments implements CanaryDeploymentInterface +type canaryDeployments struct { + client rest.Interface + ns string +} + +// newCanaryDeployments returns a CanaryDeployments +func newCanaryDeployments(c *FlaggerV1beta1Client, namespace string) *canaryDeployments { + return &canaryDeployments{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the canaryDeployment, and returns the corresponding canaryDeployment object, and an error if there is any. +func (c *canaryDeployments) Get(name string, options v1.GetOptions) (result *v1beta1.CanaryDeployment, err error) { + result = &v1beta1.CanaryDeployment{} + err = c.client.Get(). + Namespace(c.ns). + Resource("canarydeployments"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CanaryDeployments that match those selectors. +func (c *canaryDeployments) List(opts v1.ListOptions) (result *v1beta1.CanaryDeploymentList, err error) { + result = &v1beta1.CanaryDeploymentList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("canarydeployments"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested canaryDeployments. +func (c *canaryDeployments) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("canarydeployments"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a canaryDeployment and creates it. Returns the server's representation of the canaryDeployment, and an error, if there is any. +func (c *canaryDeployments) Create(canaryDeployment *v1beta1.CanaryDeployment) (result *v1beta1.CanaryDeployment, err error) { + result = &v1beta1.CanaryDeployment{} + err = c.client.Post(). + Namespace(c.ns). + Resource("canarydeployments"). + Body(canaryDeployment). + Do(). + Into(result) + return +} + +// Update takes the representation of a canaryDeployment and updates it. Returns the server's representation of the canaryDeployment, and an error, if there is any. +func (c *canaryDeployments) Update(canaryDeployment *v1beta1.CanaryDeployment) (result *v1beta1.CanaryDeployment, err error) { + result = &v1beta1.CanaryDeployment{} + err = c.client.Put(). + Namespace(c.ns). + Resource("canarydeployments"). + Name(canaryDeployment.Name). + Body(canaryDeployment). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *canaryDeployments) UpdateStatus(canaryDeployment *v1beta1.CanaryDeployment) (result *v1beta1.CanaryDeployment, err error) { + result = &v1beta1.CanaryDeployment{} + err = c.client.Put(). + Namespace(c.ns). + Resource("canarydeployments"). + Name(canaryDeployment.Name). + SubResource("status"). + Body(canaryDeployment). + Do(). + Into(result) + return +} + +// Delete takes name of the canaryDeployment and deletes it. Returns an error if one occurs. +func (c *canaryDeployments) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("canarydeployments"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *canaryDeployments) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("canarydeployments"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched canaryDeployment. +func (c *canaryDeployments) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.CanaryDeployment, err error) { + result = &v1beta1.CanaryDeployment{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("canarydeployments"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_canarydeployment.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_canarydeployment.go new file mode 100644 index 00000000..e6332f01 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_canarydeployment.go @@ -0,0 +1,140 @@ +/* +Copyright The Flagger 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 ( + v1beta1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + 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" +) + +// FakeCanaryDeployments implements CanaryDeploymentInterface +type FakeCanaryDeployments struct { + Fake *FakeFlaggerV1beta1 + ns string +} + +var canarydeploymentsResource = schema.GroupVersionResource{Group: "flagger.app", Version: "v1beta1", Resource: "canarydeployments"} + +var canarydeploymentsKind = schema.GroupVersionKind{Group: "flagger.app", Version: "v1beta1", Kind: "CanaryDeployment"} + +// Get takes name of the canaryDeployment, and returns the corresponding canaryDeployment object, and an error if there is any. +func (c *FakeCanaryDeployments) Get(name string, options v1.GetOptions) (result *v1beta1.CanaryDeployment, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(canarydeploymentsResource, c.ns, name), &v1beta1.CanaryDeployment{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CanaryDeployment), err +} + +// List takes label and field selectors, and returns the list of CanaryDeployments that match those selectors. +func (c *FakeCanaryDeployments) List(opts v1.ListOptions) (result *v1beta1.CanaryDeploymentList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(canarydeploymentsResource, canarydeploymentsKind, c.ns, opts), &v1beta1.CanaryDeploymentList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1beta1.CanaryDeploymentList{ListMeta: obj.(*v1beta1.CanaryDeploymentList).ListMeta} + for _, item := range obj.(*v1beta1.CanaryDeploymentList).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 canaryDeployments. +func (c *FakeCanaryDeployments) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(canarydeploymentsResource, c.ns, opts)) + +} + +// Create takes the representation of a canaryDeployment and creates it. Returns the server's representation of the canaryDeployment, and an error, if there is any. +func (c *FakeCanaryDeployments) Create(canaryDeployment *v1beta1.CanaryDeployment) (result *v1beta1.CanaryDeployment, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(canarydeploymentsResource, c.ns, canaryDeployment), &v1beta1.CanaryDeployment{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CanaryDeployment), err +} + +// Update takes the representation of a canaryDeployment and updates it. Returns the server's representation of the canaryDeployment, and an error, if there is any. +func (c *FakeCanaryDeployments) Update(canaryDeployment *v1beta1.CanaryDeployment) (result *v1beta1.CanaryDeployment, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(canarydeploymentsResource, c.ns, canaryDeployment), &v1beta1.CanaryDeployment{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CanaryDeployment), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCanaryDeployments) UpdateStatus(canaryDeployment *v1beta1.CanaryDeployment) (*v1beta1.CanaryDeployment, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(canarydeploymentsResource, "status", c.ns, canaryDeployment), &v1beta1.CanaryDeployment{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CanaryDeployment), err +} + +// Delete takes name of the canaryDeployment and deletes it. Returns an error if one occurs. +func (c *FakeCanaryDeployments) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(canarydeploymentsResource, c.ns, name), &v1beta1.CanaryDeployment{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCanaryDeployments) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(canarydeploymentsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1beta1.CanaryDeploymentList{}) + return err +} + +// Patch applies the patch and returns the patched canaryDeployment. +func (c *FakeCanaryDeployments) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.CanaryDeployment, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(canarydeploymentsResource, c.ns, name, data, subresources...), &v1beta1.CanaryDeployment{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CanaryDeployment), err +} diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go index b1829bd4..9b3e5f28 100644 --- a/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go +++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go @@ -32,6 +32,10 @@ func (c *FakeFlaggerV1beta1) Canaries(namespace string) v1beta1.CanaryInterface return &FakeCanaries{c, namespace} } +func (c *FakeFlaggerV1beta1) CanaryDeployments(namespace string) v1beta1.CanaryDeploymentInterface { + return &FakeCanaryDeployments{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeFlaggerV1beta1) RESTClient() rest.Interface { diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go index fe9b0e1f..205b2b51 100644 --- a/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go +++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go @@ -28,6 +28,7 @@ import ( type FlaggerV1beta1Interface interface { RESTClient() rest.Interface CanariesGetter + CanaryDeploymentsGetter } // FlaggerV1beta1Client is used to interact with features provided by the flagger.app group. @@ -39,6 +40,10 @@ func (c *FlaggerV1beta1Client) Canaries(namespace string) CanaryInterface { return newCanaries(c, namespace) } +func (c *FlaggerV1beta1Client) CanaryDeployments(namespace string) CanaryDeploymentInterface { + return newCanaryDeployments(c, namespace) +} + // NewForConfig creates a new FlaggerV1beta1Client for the given config. func NewForConfig(c *rest.Config) (*FlaggerV1beta1Client, error) { config := *c diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go index d0f1c61b..568ebe8d 100644 --- a/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go @@ -19,3 +19,5 @@ limitations under the License. package v1beta1 type CanaryExpansion interface{} + +type CanaryDeploymentExpansion interface{} diff --git a/pkg/client/informers/externalversions/flagger/v1beta1/canarydeployment.go b/pkg/client/informers/externalversions/flagger/v1beta1/canarydeployment.go new file mode 100644 index 00000000..6a84ae14 --- /dev/null +++ b/pkg/client/informers/externalversions/flagger/v1beta1/canarydeployment.go @@ -0,0 +1,89 @@ +/* +Copyright The Flagger 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 v1beta1 + +import ( + time "time" + + flaggerv1beta1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + versioned "github.com/stefanprodan/flagger/pkg/client/clientset/versioned" + internalinterfaces "github.com/stefanprodan/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1beta1 "github.com/stefanprodan/flagger/pkg/client/listers/flagger/v1beta1" + 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" +) + +// CanaryDeploymentInformer provides access to a shared informer and lister for +// CanaryDeployments. +type CanaryDeploymentInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1beta1.CanaryDeploymentLister +} + +type canaryDeploymentInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewCanaryDeploymentInformer constructs a new informer for CanaryDeployment 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 NewCanaryDeploymentInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCanaryDeploymentInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredCanaryDeploymentInformer constructs a new informer for CanaryDeployment 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 NewFilteredCanaryDeploymentInformer(client versioned.Interface, namespace string, 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.FlaggerV1beta1().CanaryDeployments(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.FlaggerV1beta1().CanaryDeployments(namespace).Watch(options) + }, + }, + &flaggerv1beta1.CanaryDeployment{}, + resyncPeriod, + indexers, + ) +} + +func (f *canaryDeploymentInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCanaryDeploymentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *canaryDeploymentInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&flaggerv1beta1.CanaryDeployment{}, f.defaultInformer) +} + +func (f *canaryDeploymentInformer) Lister() v1beta1.CanaryDeploymentLister { + return v1beta1.NewCanaryDeploymentLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/flagger/v1beta1/interface.go b/pkg/client/informers/externalversions/flagger/v1beta1/interface.go index db221d72..654bafd4 100644 --- a/pkg/client/informers/externalversions/flagger/v1beta1/interface.go +++ b/pkg/client/informers/externalversions/flagger/v1beta1/interface.go @@ -26,6 +26,8 @@ import ( type Interface interface { // Canaries returns a CanaryInformer. Canaries() CanaryInformer + // CanaryDeployments returns a CanaryDeploymentInformer. + CanaryDeployments() CanaryDeploymentInformer } type version struct { @@ -43,3 +45,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (v *version) Canaries() CanaryInformer { return &canaryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// CanaryDeployments returns a CanaryDeploymentInformer. +func (v *version) CanaryDeployments() CanaryDeploymentInformer { + return &canaryDeploymentInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index d110beb0..ca9aec23 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -55,6 +55,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=flagger.app, Version=v1beta1 case v1beta1.SchemeGroupVersion.WithResource("canaries"): return &genericInformer{resource: resource.GroupResource(), informer: f.Flagger().V1beta1().Canaries().Informer()}, nil + case v1beta1.SchemeGroupVersion.WithResource("canarydeployments"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Flagger().V1beta1().CanaryDeployments().Informer()}, nil } diff --git a/pkg/client/listers/flagger/v1beta1/canarydeployment.go b/pkg/client/listers/flagger/v1beta1/canarydeployment.go new file mode 100644 index 00000000..7a4deb0f --- /dev/null +++ b/pkg/client/listers/flagger/v1beta1/canarydeployment.go @@ -0,0 +1,94 @@ +/* +Copyright The Flagger 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 v1beta1 + +import ( + v1beta1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CanaryDeploymentLister helps list CanaryDeployments. +type CanaryDeploymentLister interface { + // List lists all CanaryDeployments in the indexer. + List(selector labels.Selector) (ret []*v1beta1.CanaryDeployment, err error) + // CanaryDeployments returns an object that can list and get CanaryDeployments. + CanaryDeployments(namespace string) CanaryDeploymentNamespaceLister + CanaryDeploymentListerExpansion +} + +// canaryDeploymentLister implements the CanaryDeploymentLister interface. +type canaryDeploymentLister struct { + indexer cache.Indexer +} + +// NewCanaryDeploymentLister returns a new CanaryDeploymentLister. +func NewCanaryDeploymentLister(indexer cache.Indexer) CanaryDeploymentLister { + return &canaryDeploymentLister{indexer: indexer} +} + +// List lists all CanaryDeployments in the indexer. +func (s *canaryDeploymentLister) List(selector labels.Selector) (ret []*v1beta1.CanaryDeployment, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1beta1.CanaryDeployment)) + }) + return ret, err +} + +// CanaryDeployments returns an object that can list and get CanaryDeployments. +func (s *canaryDeploymentLister) CanaryDeployments(namespace string) CanaryDeploymentNamespaceLister { + return canaryDeploymentNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// CanaryDeploymentNamespaceLister helps list and get CanaryDeployments. +type CanaryDeploymentNamespaceLister interface { + // List lists all CanaryDeployments in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1beta1.CanaryDeployment, err error) + // Get retrieves the CanaryDeployment from the indexer for a given namespace and name. + Get(name string) (*v1beta1.CanaryDeployment, error) + CanaryDeploymentNamespaceListerExpansion +} + +// canaryDeploymentNamespaceLister implements the CanaryDeploymentNamespaceLister +// interface. +type canaryDeploymentNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all CanaryDeployments in the indexer for a given namespace. +func (s canaryDeploymentNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.CanaryDeployment, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1beta1.CanaryDeployment)) + }) + return ret, err +} + +// Get retrieves the CanaryDeployment from the indexer for a given namespace and name. +func (s canaryDeploymentNamespaceLister) Get(name string) (*v1beta1.CanaryDeployment, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1beta1.Resource("canarydeployment"), name) + } + return obj.(*v1beta1.CanaryDeployment), nil +} diff --git a/pkg/client/listers/flagger/v1beta1/expansion_generated.go b/pkg/client/listers/flagger/v1beta1/expansion_generated.go index d6fa5287..29c8c2a3 100644 --- a/pkg/client/listers/flagger/v1beta1/expansion_generated.go +++ b/pkg/client/listers/flagger/v1beta1/expansion_generated.go @@ -25,3 +25,11 @@ type CanaryListerExpansion interface{} // CanaryNamespaceListerExpansion allows custom methods to be added to // CanaryNamespaceLister. type CanaryNamespaceListerExpansion interface{} + +// CanaryDeploymentListerExpansion allows custom methods to be added to +// CanaryDeploymentLister. +type CanaryDeploymentListerExpansion interface{} + +// CanaryDeploymentNamespaceListerExpansion allows custom methods to be added to +// CanaryDeploymentNamespaceLister. +type CanaryDeploymentNamespaceListerExpansion interface{} diff --git a/pkg/operator/deployer.go b/pkg/operator/deployer.go new file mode 100644 index 00000000..9ac9effa --- /dev/null +++ b/pkg/operator/deployer.go @@ -0,0 +1,246 @@ +package operator + +import ( + "fmt" + + istiov1alpha3 "github.com/knative/pkg/apis/istio/v1alpha3" + flaggerv1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func (c *Controller) bootstrapDeployment(cd *flaggerv1.CanaryDeployment) error { + + canaryName := cd.Spec.TargetRef.Name + primaryName := fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name) + + canaryDep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(canaryName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("deployment %s.%s not found, retrying in %v", + canaryName, cd.Namespace, c.rolloutWindow) + } else { + return err + } + } + + primaryDep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(primaryName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + primaryDep = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: primaryName, + Annotations: canaryDep.Annotations, + Namespace: cd.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cd, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryDeploymentKind, + }), + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: canaryDep.Spec.Replicas, + Strategy: canaryDep.Spec.Strategy, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": primaryName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": primaryName}, + Annotations: canaryDep.Spec.Template.Annotations, + }, + Spec: canaryDep.Spec.Template.Spec, + }, + }, + } + + _, err = c.kubeClient.AppsV1().Deployments(cd.Namespace).Create(primaryDep) + if err != nil { + return err + } + + c.recordEventInfof(cd, "Deployment %s.%s created", primaryDep.GetName(), cd.Namespace) + } + + if cd.Status.State == "" { + c.scaleToZeroCanary(cd) + } + + canaryService, err := c.kubeClient.CoreV1().Services(cd.Namespace).Get(canaryName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + canaryService = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: canaryName, + Namespace: cd.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cd, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryDeploymentKind, + }), + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{"app": canaryName}, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: cd.Spec.Service.Port, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: cd.Spec.Service.Port, + }, + }, + }, + }, + } + + _, err = c.kubeClient.CoreV1().Services(cd.Namespace).Create(canaryService) + if err != nil { + return err + } + c.recordEventInfof(cd, "Service %s.%s created", canaryService.GetName(), cd.Namespace) + } + + canaryTestServiceName := fmt.Sprintf("%s-canary", cd.Spec.TargetRef.Name) + canaryTestService, err := c.kubeClient.CoreV1().Services(cd.Namespace).Get(canaryTestServiceName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + canaryTestService = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: canaryTestServiceName, + Namespace: cd.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cd, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryDeploymentKind, + }), + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{"app": canaryName}, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: cd.Spec.Service.Port, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: cd.Spec.Service.Port, + }, + }, + }, + }, + } + + _, err = c.kubeClient.CoreV1().Services(cd.Namespace).Create(canaryTestService) + if err != nil { + return err + } + c.recordEventInfof(cd, "Service %s.%s created", canaryTestService.GetName(), cd.Namespace) + } + + primaryService, err := c.kubeClient.CoreV1().Services(cd.Namespace).Get(primaryName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + primaryService = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: primaryName, + Namespace: cd.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cd, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryDeploymentKind, + }), + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{"app": primaryName}, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: cd.Spec.Service.Port, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: cd.Spec.Service.Port, + }, + }, + }, + }, + } + + _, err = c.kubeClient.CoreV1().Services(cd.Namespace).Create(primaryService) + if err != nil { + return err + } + + c.recordEventInfof(cd, "Service %s.%s created", primaryService.GetName(), cd.Namespace) + } + + hosts := append(cd.Spec.Service.Hosts, canaryName) + gateways := append(cd.Spec.Service.Gateways, "mesh") + virtualService, err := c.istioClient.NetworkingV1alpha3().VirtualServices(cd.Namespace).Get(canaryName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + virtualService = &istiov1alpha3.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Name: canaryName, + Namespace: cd.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cd, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryDeploymentKind, + }), + }, + }, + Spec: istiov1alpha3.VirtualServiceSpec{ + Hosts: hosts, + Gateways: gateways, + Http: []istiov1alpha3.HTTPRoute{ + { + Route: []istiov1alpha3.DestinationWeight{ + { + Destination: istiov1alpha3.Destination{ + Host: primaryName, + Port: istiov1alpha3.PortSelector{ + Number: uint32(cd.Spec.Service.Port), + }, + }, + Weight: 100, + }, + { + Destination: istiov1alpha3.Destination{ + Host: canaryName, + Port: istiov1alpha3.PortSelector{ + Number: uint32(cd.Spec.Service.Port), + }, + }, + Weight: 0, + }, + }, + }, + }, + }, + } + + _, err = c.istioClient.NetworkingV1alpha3().VirtualServices(cd.Namespace).Create(virtualService) + if err != nil { + return err + } + c.recordEventInfof(cd, "VirtualService %s.%s created", virtualService.GetName(), cd.Namespace) + } + + return nil +} diff --git a/pkg/operator/deployment.go b/pkg/operator/deployment.go new file mode 100644 index 00000000..57ffd4c1 --- /dev/null +++ b/pkg/operator/deployment.go @@ -0,0 +1,422 @@ +package operator + +import ( + "fmt" + "time" + + istiov1alpha3 "github.com/knative/pkg/apis/istio/v1alpha3" + flaggerv1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (c *Controller) doRollouts() { + c.rollouts.Range(func(key interface{}, value interface{}) bool { + r := value.(*flaggerv1.CanaryDeployment) + if r.Spec.TargetRef.Kind == "Deployment" { + go c.advanceDeploymentRollout(r.Name, r.Namespace) + } + return true + }) +} + +func (c *Controller) advanceDeploymentRollout(name string, namespace string) { + // gate stage: check if the rollout exists + r, ok := c.getRollout(name, namespace) + if !ok { + return + } + + err := c.bootstrapDeployment(r) + if err != nil { + c.recordEventWarningf(r, "%v", err) + return + } + + // set max weight default value to 100% + maxWeight := 100 + if r.Spec.CanaryAnalysis.MaxWeight > 0 { + maxWeight = r.Spec.CanaryAnalysis.MaxWeight + } + + // gate stage: check if canary deployment exists and is healthy + canary, ok := c.getCanaryDeployment(r, r.Spec.TargetRef.Name, r.Namespace) + if !ok { + return + } + + // gate stage: check if primary deployment exists and is healthy + primary, ok := c.getDeployment(r, fmt.Sprintf("%s-primary", r.Spec.TargetRef.Name), r.Namespace) + if !ok { + return + } + + // gate stage: check if virtual service exists + // and if it contains weighted destination routes to the primary and canary services + vs, primaryRoute, canaryRoute, ok := c.getVirtualService(r) + if !ok { + return + } + + // gate stage: check if rollout should start (canary revision has changes) or continue + if ok := c.checkRolloutStatus(r, canary); !ok { + return + } + + // gate stage: check if the number of failed checks reached the threshold + if r.Status.State == "running" && r.Status.FailedChecks >= r.Spec.CanaryAnalysis.Threshold { + c.recordEventWarningf(r, "Rolling back %s.%s failed checks threshold reached %v", + r.Name, r.Namespace, r.Status.FailedChecks) + + // route all traffic back to primary + primaryRoute.Weight = 100 + canaryRoute.Weight = 0 + if ok := c.updateVirtualServiceRoutes(r, vs, primaryRoute, canaryRoute); !ok { + return + } + + c.recordEventWarningf(r, "Canary failed! Scaling down %s.%s", + canary.GetName(), canary.Namespace) + + // shutdown canary + c.scaleToZeroCanary(r) + + // mark rollout as failed + c.updateRolloutStatus(r, "promotion-failed") + return + } + + // gate stage: check if the canary success rate is above the threshold + // skip check if no traffic is routed to canary + if canaryRoute.Weight == 0 { + c.recordEventInfof(r, "Starting canary deployment for %s.%s", r.Name, r.Namespace) + } else { + if ok := c.checkDeploymentMetrics(r); !ok { + c.updateRolloutFailedChecks(r, r.Status.FailedChecks+1) + return + } + } + + // routing stage: increase canary traffic percentage + if canaryRoute.Weight < maxWeight { + primaryRoute.Weight -= r.Spec.CanaryAnalysis.StepWeight + if primaryRoute.Weight < 0 { + primaryRoute.Weight = 0 + } + canaryRoute.Weight += r.Spec.CanaryAnalysis.StepWeight + if primaryRoute.Weight > 100 { + primaryRoute.Weight = 100 + } + + if ok := c.updateVirtualServiceRoutes(r, vs, primaryRoute, canaryRoute); !ok { + return + } + + c.recordEventInfof(r, "Advance %s.%s canary weight %v", r.Name, r.Namespace, canaryRoute.Weight) + + // promotion stage: override primary.template.spec with the canary spec + if canaryRoute.Weight == maxWeight { + c.recordEventInfof(r, "Copying %s.%s template spec to %s.%s", + canary.GetName(), canary.Namespace, primary.GetName(), primary.Namespace) + + primary.Spec.Template.Spec = canary.Spec.Template.Spec + _, err := c.kubeClient.AppsV1().Deployments(primary.Namespace).Update(primary) + if err != nil { + c.recordEventErrorf(r, "Updating template spec %s.%s failed: %v", primary.GetName(), primary.Namespace, err) + return + } + } + } else { + // final stage: route all traffic back to primary + primaryRoute.Weight = 100 + canaryRoute.Weight = 0 + if ok := c.updateVirtualServiceRoutes(r, vs, primaryRoute, canaryRoute); !ok { + return + } + + // final stage: mark rollout as finished and scale canary to zero replicas + c.recordEventInfof(r, "Scaling down %s.%s", canary.GetName(), canary.Namespace) + c.scaleToZeroCanary(r) + c.updateRolloutStatus(r, "promotion-finished") + } +} + +func (c *Controller) getRollout(name string, namespace string) (*flaggerv1.CanaryDeployment, bool) { + r, err := c.rolloutClient.FlaggerV1beta1().CanaryDeployments(namespace).Get(name, v1.GetOptions{}) + if err != nil { + c.logger.Errorf("CanaryDeployment %s.%s not found", name, namespace) + return nil, false + } + + return r, true +} + +func (c *Controller) checkRolloutStatus(r *flaggerv1.CanaryDeployment, canary *appsv1.Deployment) bool { + canaryRevision, err := c.getDeploymentSpecEnc(canary) + if err != nil { + c.logger.Errorf("Canary %s.%s not found: %v", r.Name, r.Namespace, err) + return false + } + + if r.Status.State == "" { + r.Status = flaggerv1.CanaryDeploymentStatus{ + State: "initialized", + CanaryRevision: canaryRevision, + FailedChecks: 0, + } + r, err = c.rolloutClient.FlaggerV1beta1().CanaryDeployments(r.Namespace).Update(r) + if err != nil { + c.logger.Errorf("Canary %s.%s status update failed: %v", r.Name, r.Namespace, err) + return false + } + + c.recordEventInfof(r, "Initialization done! %s.%ss", canary.GetName(), canary.Namespace) + return false + } + + if r.Status.State == "running" { + return true + } + + if r.Status.State == "promotion-finished" { + c.setCanaryRevision(r, canary, "finished") + c.logger.Infof("Promotion completed! %s.%s", r.Name, r.Namespace) + return false + } + + if r.Status.State == "promotion-failed" { + c.setCanaryRevision(r, canary, "failed") + c.logger.Infof("Promotion failed! %s.%s", r.Name, r.Namespace) + return false + } + + if diff, err := c.diffDeploymentSpec(r, canary); diff { + c.recordEventInfof(r, "New revision detected %s.%s", + canary.GetName(), canary.Namespace) + canary.Spec.Replicas = int32p(1) + canary, err = c.kubeClient.AppsV1().Deployments(canary.Namespace).Update(canary) + if err != nil { + c.recordEventErrorf(r, "Scaling up %s.%s failed: %v", canary.GetName(), canary.Namespace, err) + return false + } + + r.Status = flaggerv1.CanaryDeploymentStatus{ + FailedChecks: 0, + } + c.setCanaryRevision(r, canary, "running") + c.recordEventInfof(r, "Scaling up %s.%s", canary.GetName(), canary.Namespace) + + return false + } + + return false +} + +func (c *Controller) updateRolloutStatus(r *flaggerv1.CanaryDeployment, status string) bool { + var err error + r.Status.State = status + r, err = c.rolloutClient.FlaggerV1beta1().CanaryDeployments(r.Namespace).Update(r) + if err != nil { + c.logger.Errorf("Canary %s.%s status update failed: %v", r.Name, r.Namespace, err) + return false + } + return true +} + +func (c *Controller) updateRolloutFailedChecks(r *flaggerv1.CanaryDeployment, val int) bool { + var err error + r.Status.FailedChecks = val + r, err = c.rolloutClient.FlaggerV1beta1().CanaryDeployments(r.Namespace).Update(r) + if err != nil { + c.logger.Errorf("Canary %s.%s status update failed: %v", r.Name, r.Namespace, err) + return false + } + return true +} + +func (c *Controller) getDeployment(r *flaggerv1.CanaryDeployment, name string, namespace string) (*appsv1.Deployment, bool) { + dep, err := c.kubeClient.AppsV1().Deployments(namespace).Get(name, v1.GetOptions{}) + if err != nil { + c.recordEventErrorf(r, "Deployment %s.%s not found", name, namespace) + return nil, false + } + + if msg, healthy := getDeploymentStatus(dep); !healthy { + c.recordEventWarningf(r, "Halt %s.%s advancement %s", dep.GetName(), dep.Namespace, msg) + return nil, false + } + + if dep.Spec.Replicas == nil || *dep.Spec.Replicas == 0 { + return nil, false + } + + return dep, true +} + +func (c *Controller) getCanaryDeployment(r *flaggerv1.CanaryDeployment, name string, namespace string) (*appsv1.Deployment, bool) { + dep, err := c.kubeClient.AppsV1().Deployments(namespace).Get(name, v1.GetOptions{}) + if err != nil { + c.recordEventErrorf(r, "Deployment %s.%s not found", name, namespace) + return nil, false + } + + if msg, healthy := getDeploymentStatus(dep); !healthy { + c.recordEventWarningf(r, "Halt %s.%s advancement %s", dep.GetName(), dep.Namespace, msg) + return nil, false + } + + return dep, true +} + +func (c *Controller) checkDeploymentMetrics(r *flaggerv1.CanaryDeployment) bool { + for _, metric := range r.Spec.CanaryAnalysis.Metrics { + if metric.Name == "istio_requests_total" { + val, err := c.getDeploymentCounter(r.Spec.TargetRef.Name, r.Namespace, metric.Name, metric.Interval) + if err != nil { + c.recordEventErrorf(r, "Metrics server %s query failed: %v", c.metricsServer, err) + return false + } + if float64(metric.Threshold) > val { + c.recordEventWarningf(r, "Halt %s.%s advancement success rate %.2f%% < %v%%", + r.Name, r.Namespace, val, metric.Threshold) + return false + } + } + + if metric.Name == "istio_request_duration_seconds_bucket" { + val, err := c.GetDeploymentHistogram(r.Spec.TargetRef.Name, r.Namespace, metric.Name, metric.Interval) + if err != nil { + c.recordEventErrorf(r, "Metrics server %s query failed: %v", c.metricsServer, err) + return false + } + t := time.Duration(metric.Threshold) * time.Millisecond + if val > t { + c.recordEventWarningf(r, "Halt %s.%s advancement request duration %v > %v", + r.Name, r.Namespace, val, t) + return false + } + } + } + + return true +} + +func (c *Controller) scaleToZeroCanary(r *flaggerv1.CanaryDeployment) { + canary, err := c.kubeClient.AppsV1().Deployments(r.Namespace).Get(r.Spec.TargetRef.Name, v1.GetOptions{}) + if err != nil { + c.recordEventErrorf(r, "Deployment %s.%s not found", r.Spec.TargetRef.Name, r.Namespace) + return + } + //HPA https://github.com/kubernetes/kubernetes/pull/29212 + canary.Spec.Replicas = int32p(0) + canary, err = c.kubeClient.AppsV1().Deployments(canary.Namespace).Update(canary) + if err != nil { + c.recordEventErrorf(r, "Scaling down %s.%s failed: %v", canary.GetName(), canary.Namespace, err) + return + } +} + +func (c *Controller) setCanaryRevision(r *flaggerv1.CanaryDeployment, canary *appsv1.Deployment, status string) { + r.Status = flaggerv1.CanaryDeploymentStatus{ + State: status, + FailedChecks: r.Status.FailedChecks, + } + err := c.saveDeploymentSpec(r, canary) + if err != nil { + c.logger.Errorf("CanaryDeployment %s.%s status update failed: %v", r.Name, r.Namespace, err) + } +} + +func (c *Controller) getVirtualService(r *flaggerv1.CanaryDeployment) ( + vs *istiov1alpha3.VirtualService, + primary istiov1alpha3.DestinationWeight, + canary istiov1alpha3.DestinationWeight, + ok bool, +) { + var err error + vs, err = c.istioClient.NetworkingV1alpha3().VirtualServices(r.Namespace).Get(r.Name, v1.GetOptions{}) + if err != nil { + c.recordEventErrorf(r, "VirtualService %s.%s not found", r.Name, r.Namespace) + return + } + + for _, http := range vs.Spec.Http { + for _, route := range http.Route { + if route.Destination.Host == fmt.Sprintf("%s-primary", r.Spec.TargetRef.Name) { + primary = route + } + if route.Destination.Host == r.Spec.TargetRef.Name { + canary = route + } + } + } + + if primary.Weight == 0 && canary.Weight == 0 { + c.recordEventErrorf(r, "VirtualService %s.%s does not contain routes for %s and %s", + r.Name, r.Namespace, fmt.Sprintf("%s-primary", r.Spec.TargetRef.Name), r.Spec.TargetRef.Name) + return + } + + ok = true + return +} + +func (c *Controller) updateVirtualServiceRoutes( + r *flaggerv1.CanaryDeployment, + vs *istiov1alpha3.VirtualService, + primary istiov1alpha3.DestinationWeight, + canary istiov1alpha3.DestinationWeight, +) bool { + vs.Spec.Http = []istiov1alpha3.HTTPRoute{ + { + Route: []istiov1alpha3.DestinationWeight{primary, canary}, + }, + } + + var err error + vs, err = c.istioClient.NetworkingV1alpha3().VirtualServices(r.Namespace).Update(vs) + if err != nil { + c.recordEventErrorf(r, "VirtualService %s.%s update failed: %v", r.Name, r.Namespace, err) + return false + } + return true +} + +func getDeploymentStatus(deployment *appsv1.Deployment) (string, bool) { + if deployment.Generation <= deployment.Status.ObservedGeneration { + cond := getDeploymentCondition(deployment.Status, appsv1.DeploymentProgressing) + if cond != nil && cond.Reason == "ProgressDeadlineExceeded" { + return fmt.Sprintf("deployment %q exceeded its progress deadline", deployment.GetName()), false + } else if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas { + return fmt.Sprintf("waiting for rollout to finish: %d out of %d new replicas have been updated", + deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas), false + } else if deployment.Status.Replicas > deployment.Status.UpdatedReplicas { + return fmt.Sprintf("waiting for rollout to finish: %d old replicas are pending termination", + deployment.Status.Replicas-deployment.Status.UpdatedReplicas), false + } else if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas { + return fmt.Sprintf("waiting for rollout to finish: %d of %d updated replicas are available", + deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas), false + } + } else { + return "waiting for rollout to finish: observed deployment generation less then desired generation", false + } + + return "ready", true +} + +func getDeploymentCondition( + status appsv1.DeploymentStatus, + conditionType appsv1.DeploymentConditionType, +) *appsv1.DeploymentCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == conditionType { + return &c + } + } + return nil +} + +func int32p(i int32) *int32 { + return &i +} diff --git a/pkg/operator/observer.go b/pkg/operator/observer.go new file mode 100644 index 00000000..d00ce355 --- /dev/null +++ b/pkg/operator/observer.go @@ -0,0 +1,172 @@ +package operator + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "time" +) + +type VectorQueryResponse struct { + Data struct { + Result []struct { + Metric struct { + Code string `json:"response_code"` + Name string `json:"destination_workload"` + } + Value []interface{} `json:"value"` + } + } +} + +func (c *Controller) queryMetric(query string) (*VectorQueryResponse, error) { + promURL, err := url.Parse(c.metricsServer) + if err != nil { + return nil, err + } + + u, err := url.Parse(fmt.Sprintf("./api/v1/query?query=%s", query)) + if err != nil { + return nil, err + } + + u = promURL.ResolveReference(u) + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) + defer cancel() + + r, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + defer r.Body.Close() + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("error reading body: %s", err.Error()) + } + + if 400 <= r.StatusCode { + return nil, fmt.Errorf("error response: %s", string(b)) + } + + var values VectorQueryResponse + err = json.Unmarshal(b, &values) + if err != nil { + return nil, fmt.Errorf("error unmarshaling result: %s, '%s'", err.Error(), string(b)) + } + + return &values, nil +} + +// istio_requests_total +func (c *Controller) getDeploymentCounter(name string, namespace string, metric string, interval string) (float64, error) { + var rate *float64 + querySt := url.QueryEscape(`sum(rate(` + + metric + `{reporter="destination",destination_workload_namespace=~"` + + namespace + `",destination_workload=~"` + + name + `",response_code!~"5.*"}[1m])) / sum(rate(` + + metric + `{reporter="destination",destination_workload_namespace=~"` + + namespace + `",destination_workload=~"` + + name + `"}[` + + interval + `])) * 100 `) + result, err := c.queryMetric(querySt) + if err != nil { + return 0, err + } + + for _, v := range result.Data.Result { + metricValue := v.Value[1] + switch metricValue.(type) { + case string: + f, err := strconv.ParseFloat(metricValue.(string), 64) + if err != nil { + return 0, err + } + rate = &f + } + } + if rate == nil { + return 0, fmt.Errorf("no values found for metric %s", metric) + } + return *rate, nil +} + +// istio_request_duration_seconds_bucket +func (c *Controller) GetDeploymentHistogram(name string, namespace string, metric string, interval string) (time.Duration, error) { + var rate *float64 + querySt := url.QueryEscape(`histogram_quantile(0.99, sum(rate(` + + metric + `{reporter="destination",destination_workload=~"` + + name + `", destination_workload_namespace=~"` + + namespace + `"}[` + + interval + `])) by (le))`) + result, err := c.queryMetric(querySt) + if err != nil { + return 0, err + } + + for _, v := range result.Data.Result { + metricValue := v.Value[1] + switch metricValue.(type) { + case string: + f, err := strconv.ParseFloat(metricValue.(string), 64) + if err != nil { + return 0, err + } + rate = &f + } + } + if rate == nil { + return 0, fmt.Errorf("no values found for metric %s", metric) + } + ms := time.Duration(int64(*rate*1000)) * time.Millisecond + return ms, nil +} + +func CheckMetricsServer(address string) (bool, error) { + promURL, err := url.Parse(address) + if err != nil { + return false, err + } + + u, err := url.Parse("./api/v1/status/flags") + if err != nil { + return false, err + } + + u = promURL.ResolveReference(u) + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return false, err + } + + ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) + defer cancel() + + r, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return false, err + } + defer r.Body.Close() + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return false, fmt.Errorf("error reading body: %s", err.Error()) + } + + if 400 <= r.StatusCode { + return false, fmt.Errorf("error response: %s", string(b)) + } + + return true, nil +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go new file mode 100644 index 00000000..56ff4043 --- /dev/null +++ b/pkg/operator/operator.go @@ -0,0 +1,229 @@ +package operator + +import ( + "fmt" + "sync" + "time" + + "github.com/google/go-cmp/cmp" + istioclientset "github.com/knative/pkg/client/clientset/versioned" + flaggerv1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + clientset "github.com/stefanprodan/flagger/pkg/client/clientset/versioned" + flaggerscheme "github.com/stefanprodan/flagger/pkg/client/clientset/versioned/scheme" + flaggerinformers "github.com/stefanprodan/flagger/pkg/client/informers/externalversions/flagger/v1beta1" + flaggerlisters "github.com/stefanprodan/flagger/pkg/client/listers/flagger/v1beta1" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const controllerAgentName = "flagger" + +type Controller struct { + kubeClient kubernetes.Interface + istioClient istioclientset.Interface + rolloutClient clientset.Interface + rolloutLister flaggerlisters.CanaryDeploymentLister + rolloutSynced cache.InformerSynced + rolloutWindow time.Duration + workqueue workqueue.RateLimitingInterface + recorder record.EventRecorder + logger *zap.SugaredLogger + metricsServer string + rollouts *sync.Map +} + +func NewController( + kubeClient kubernetes.Interface, + istioClient istioclientset.Interface, + rolloutClient clientset.Interface, + rolloutInformer flaggerinformers.CanaryDeploymentInformer, + rolloutWindow time.Duration, + metricServer string, + logger *zap.SugaredLogger, + +) *Controller { + logger.Debug("Creating event broadcaster") + flaggerscheme.AddToScheme(scheme.Scheme) + eventBroadcaster := record.NewBroadcaster() + eventBroadcaster.StartLogging(logger.Named("event-broadcaster").Debugf) + eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{ + Interface: kubeClient.CoreV1().Events(""), + }) + recorder := eventBroadcaster.NewRecorder( + scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) + + ctrl := &Controller{ + kubeClient: kubeClient, + istioClient: istioClient, + rolloutClient: rolloutClient, + rolloutLister: rolloutInformer.Lister(), + rolloutSynced: rolloutInformer.Informer().HasSynced, + workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName), + recorder: recorder, + logger: logger, + rollouts: new(sync.Map), + metricsServer: metricServer, + rolloutWindow: rolloutWindow, + } + + rolloutInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: ctrl.enqueue, + UpdateFunc: func(old, new interface{}) { + oldRoll, ok := checkCustomResourceType(old, logger) + if !ok { + return + } + newRoll, ok := checkCustomResourceType(new, logger) + if !ok { + return + } + + if diff := cmp.Diff(newRoll.Spec, oldRoll.Spec); diff != "" { + ctrl.logger.Debugf("Diff detected %s.%s %s", oldRoll.Name, oldRoll.Namespace, diff) + ctrl.enqueue(new) + } + }, + DeleteFunc: func(old interface{}) { + r, ok := checkCustomResourceType(old, logger) + if ok { + ctrl.logger.Infof("Deleting %s.%s from cache", r.Name, r.Namespace) + ctrl.rollouts.Delete(fmt.Sprintf("%s.%s", r.Name, r.Namespace)) + } + }, + }) + + return ctrl +} + +func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { + defer utilruntime.HandleCrash() + defer c.workqueue.ShutDown() + + c.logger.Info("Starting operator") + + for i := 0; i < threadiness; i++ { + go wait.Until(func() { + for c.processNextWorkItem() { + } + }, time.Second, stopCh) + } + + c.logger.Info("Started operator workers") + + tickChan := time.NewTicker(c.rolloutWindow).C + for { + select { + case <-tickChan: + c.doRollouts() + case <-stopCh: + c.logger.Info("Shutting down operator workers") + return nil + } + } + + return nil +} + +func (c *Controller) processNextWorkItem() bool { + obj, shutdown := c.workqueue.Get() + + if shutdown { + return false + } + + err := func(obj interface{}) error { + defer c.workqueue.Done(obj) + var key string + var ok bool + if key, ok = obj.(string); !ok { + c.workqueue.Forget(obj) + utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) + return nil + } + // Run the syncHandler, passing it the namespace/name string of the + // Foo resource to be synced. + if err := c.syncHandler(key); err != nil { + return fmt.Errorf("error syncing '%s': %s", key, err.Error()) + } + // Finally, if no error occurs we Forget this item so it does not + // get queued again until another change happens. + c.workqueue.Forget(obj) + return nil + }(obj) + + if err != nil { + utilruntime.HandleError(err) + return true + } + + return true +} + +func (c *Controller) syncHandler(key string) error { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key)) + return nil + } + cd, err := c.rolloutLister.CanaryDeployments(namespace).Get(name) + if errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("'%s' in work queue no longer exists", key)) + return nil + } + + c.rollouts.Store(fmt.Sprintf("%s.%s", cd.Name, cd.Namespace), cd) + + err = c.bootstrapDeployment(cd) + if err != nil { + c.logger.Warnf("%s.%s bootstrap error %v", cd.Name, cd.Namespace, err) + return err + } + + c.logger.Infof("Synced %s", key) + + return nil +} + +func (c *Controller) enqueue(obj interface{}) { + var key string + var err error + if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { + utilruntime.HandleError(err) + return + } + c.workqueue.AddRateLimited(key) +} + +func checkCustomResourceType(obj interface{}, logger *zap.SugaredLogger) (flaggerv1.CanaryDeployment, bool) { + var roll *flaggerv1.CanaryDeployment + var ok bool + if roll, ok = obj.(*flaggerv1.CanaryDeployment); !ok { + logger.Errorf("Event Watch received an invalid object: %#v", obj) + return flaggerv1.CanaryDeployment{}, false + } + return *roll, true +} + +func (c *Controller) recordEventInfof(r *flaggerv1.CanaryDeployment, template string, args ...interface{}) { + c.logger.Infof(template, args...) + c.recorder.Event(r, corev1.EventTypeNormal, "Synced", fmt.Sprintf(template, args...)) +} + +func (c *Controller) recordEventErrorf(r *flaggerv1.CanaryDeployment, template string, args ...interface{}) { + c.logger.Errorf(template, args...) + c.recorder.Event(r, corev1.EventTypeWarning, "Synced", fmt.Sprintf(template, args...)) +} + +func (c *Controller) recordEventWarningf(r *flaggerv1.CanaryDeployment, template string, args ...interface{}) { + c.logger.Infof(template, args...) + c.recorder.Event(r, corev1.EventTypeWarning, "Synced", fmt.Sprintf(template, args...)) +} diff --git a/pkg/operator/utils.go b/pkg/operator/utils.go new file mode 100644 index 00000000..6fbde4bf --- /dev/null +++ b/pkg/operator/utils.go @@ -0,0 +1,79 @@ +package operator + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + flaggerv1 "github.com/stefanprodan/flagger/pkg/apis/flagger/v1beta1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (c *Controller) saveDeploymentSpec(cd *flaggerv1.CanaryDeployment, dep *appsv1.Deployment) error { + specJson, err := json.Marshal(dep.Spec.Template.Spec) + if err != nil { + return err + } + + specEnc := base64.StdEncoding.EncodeToString(specJson) + cd.Status.CanaryRevision = specEnc + cd, err = c.rolloutClient.FlaggerV1beta1().CanaryDeployments(cd.Namespace).Update(cd) + if err != nil { + return err + } + return nil +} + +func (c *Controller) diffDeploymentSpec(cd *flaggerv1.CanaryDeployment, dep *appsv1.Deployment) (bool, error) { + if cd.Status.CanaryRevision == "" { + return true, nil + } + + newSpec := &dep.Spec.Template.Spec + oldSpecJson, err := base64.StdEncoding.DecodeString(cd.Status.CanaryRevision) + if err != nil { + return false, err + } + oldSpec := &corev1.PodSpec{} + err = json.Unmarshal(oldSpecJson, oldSpec) + if err != nil { + return false, err + } + + if diff := cmp.Diff(*newSpec, *oldSpec, cmpopts.IgnoreUnexported(resource.Quantity{})); diff != "" { + fmt.Println(diff) + return true, nil + } + + return false, nil +} + +func (c *Controller) getDeploymentSpec(name string, namespace string) (string, error) { + dep, err := c.kubeClient.AppsV1().Deployments(namespace).Get(name, v1.GetOptions{}) + if err != nil { + return "", err + } + + specJson, err := json.Marshal(dep.Spec.Template.Spec) + if err != nil { + return "", err + } + + specEnc := base64.StdEncoding.EncodeToString(specJson) + return specEnc, nil +} + +func (c *Controller) getDeploymentSpecEnc(dep *appsv1.Deployment) (string, error) { + specJson, err := json.Marshal(dep.Spec.Template.Spec) + if err != nil { + return "", err + } + + specEnc := base64.StdEncoding.EncodeToString(specJson) + return specEnc, nil +} diff --git a/vendor/github.com/fatih/color/LICENSE.md b/vendor/github.com/fatih/color/LICENSE.md deleted file mode 100644 index 25fdaf63..00000000 --- a/vendor/github.com/fatih/color/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Fatih Arslan - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/fatih/color/color.go b/vendor/github.com/fatih/color/color.go deleted file mode 100644 index 91c8e9f0..00000000 --- a/vendor/github.com/fatih/color/color.go +++ /dev/null @@ -1,603 +0,0 @@ -package color - -import ( - "fmt" - "io" - "os" - "strconv" - "strings" - "sync" - - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" -) - -var ( - // NoColor defines if the output is colorized or not. It's dynamically set to - // false or true based on the stdout's file descriptor referring to a terminal - // or not. This is a global option and affects all colors. For more control - // over each color block use the methods DisableColor() individually. - NoColor = os.Getenv("TERM") == "dumb" || - (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) - - // Output defines the standard output of the print functions. By default - // os.Stdout is used. - Output = colorable.NewColorableStdout() - - // Error defines a color supporting writer for os.Stderr. - Error = colorable.NewColorableStderr() - - // colorsCache is used to reduce the count of created Color objects and - // allows to reuse already created objects with required Attribute. - colorsCache = make(map[Attribute]*Color) - colorsCacheMu sync.Mutex // protects colorsCache -) - -// Color defines a custom color object which is defined by SGR parameters. -type Color struct { - params []Attribute - noColor *bool -} - -// Attribute defines a single SGR Code -type Attribute int - -const escape = "\x1b" - -// Base attributes -const ( - Reset Attribute = iota - Bold - Faint - Italic - Underline - BlinkSlow - BlinkRapid - ReverseVideo - Concealed - CrossedOut -) - -// Foreground text colors -const ( - FgBlack Attribute = iota + 30 - FgRed - FgGreen - FgYellow - FgBlue - FgMagenta - FgCyan - FgWhite -) - -// Foreground Hi-Intensity text colors -const ( - FgHiBlack Attribute = iota + 90 - FgHiRed - FgHiGreen - FgHiYellow - FgHiBlue - FgHiMagenta - FgHiCyan - FgHiWhite -) - -// Background text colors -const ( - BgBlack Attribute = iota + 40 - BgRed - BgGreen - BgYellow - BgBlue - BgMagenta - BgCyan - BgWhite -) - -// Background Hi-Intensity text colors -const ( - BgHiBlack Attribute = iota + 100 - BgHiRed - BgHiGreen - BgHiYellow - BgHiBlue - BgHiMagenta - BgHiCyan - BgHiWhite -) - -// New returns a newly created color object. -func New(value ...Attribute) *Color { - c := &Color{params: make([]Attribute, 0)} - c.Add(value...) - return c -} - -// Set sets the given parameters immediately. It will change the color of -// output with the given SGR parameters until color.Unset() is called. -func Set(p ...Attribute) *Color { - c := New(p...) - c.Set() - return c -} - -// Unset resets all escape attributes and clears the output. Usually should -// be called after Set(). -func Unset() { - if NoColor { - return - } - - fmt.Fprintf(Output, "%s[%dm", escape, Reset) -} - -// Set sets the SGR sequence. -func (c *Color) Set() *Color { - if c.isNoColorSet() { - return c - } - - fmt.Fprintf(Output, c.format()) - return c -} - -func (c *Color) unset() { - if c.isNoColorSet() { - return - } - - Unset() -} - -func (c *Color) setWriter(w io.Writer) *Color { - if c.isNoColorSet() { - return c - } - - fmt.Fprintf(w, c.format()) - return c -} - -func (c *Color) unsetWriter(w io.Writer) { - if c.isNoColorSet() { - return - } - - if NoColor { - return - } - - fmt.Fprintf(w, "%s[%dm", escape, Reset) -} - -// Add is used to chain SGR parameters. Use as many as parameters to combine -// and create custom color objects. Example: Add(color.FgRed, color.Underline). -func (c *Color) Add(value ...Attribute) *Color { - c.params = append(c.params, value...) - return c -} - -func (c *Color) prepend(value Attribute) { - c.params = append(c.params, 0) - copy(c.params[1:], c.params[0:]) - c.params[0] = value -} - -// Fprint formats using the default formats for its operands and writes to w. -// Spaces are added between operands when neither is a string. -// It returns the number of bytes written and any write error encountered. -// On Windows, users should wrap w with colorable.NewColorable() if w is of -// type *os.File. -func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { - c.setWriter(w) - defer c.unsetWriter(w) - - return fmt.Fprint(w, a...) -} - -// Print formats using the default formats for its operands and writes to -// standard output. Spaces are added between operands when neither is a -// string. It returns the number of bytes written and any write error -// encountered. This is the standard fmt.Print() method wrapped with the given -// color. -func (c *Color) Print(a ...interface{}) (n int, err error) { - c.Set() - defer c.unset() - - return fmt.Fprint(Output, a...) -} - -// Fprintf formats according to a format specifier and writes to w. -// It returns the number of bytes written and any write error encountered. -// On Windows, users should wrap w with colorable.NewColorable() if w is of -// type *os.File. -func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { - c.setWriter(w) - defer c.unsetWriter(w) - - return fmt.Fprintf(w, format, a...) -} - -// Printf formats according to a format specifier and writes to standard output. -// It returns the number of bytes written and any write error encountered. -// This is the standard fmt.Printf() method wrapped with the given color. -func (c *Color) Printf(format string, a ...interface{}) (n int, err error) { - c.Set() - defer c.unset() - - return fmt.Fprintf(Output, format, a...) -} - -// Fprintln formats using the default formats for its operands and writes to w. -// Spaces are always added between operands and a newline is appended. -// On Windows, users should wrap w with colorable.NewColorable() if w is of -// type *os.File. -func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { - c.setWriter(w) - defer c.unsetWriter(w) - - return fmt.Fprintln(w, a...) -} - -// Println formats using the default formats for its operands and writes to -// standard output. Spaces are always added between operands and a newline is -// appended. It returns the number of bytes written and any write error -// encountered. This is the standard fmt.Print() method wrapped with the given -// color. -func (c *Color) Println(a ...interface{}) (n int, err error) { - c.Set() - defer c.unset() - - return fmt.Fprintln(Output, a...) -} - -// Sprint is just like Print, but returns a string instead of printing it. -func (c *Color) Sprint(a ...interface{}) string { - return c.wrap(fmt.Sprint(a...)) -} - -// Sprintln is just like Println, but returns a string instead of printing it. -func (c *Color) Sprintln(a ...interface{}) string { - return c.wrap(fmt.Sprintln(a...)) -} - -// Sprintf is just like Printf, but returns a string instead of printing it. -func (c *Color) Sprintf(format string, a ...interface{}) string { - return c.wrap(fmt.Sprintf(format, a...)) -} - -// FprintFunc returns a new function that prints the passed arguments as -// colorized with color.Fprint(). -func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) { - return func(w io.Writer, a ...interface{}) { - c.Fprint(w, a...) - } -} - -// PrintFunc returns a new function that prints the passed arguments as -// colorized with color.Print(). -func (c *Color) PrintFunc() func(a ...interface{}) { - return func(a ...interface{}) { - c.Print(a...) - } -} - -// FprintfFunc returns a new function that prints the passed arguments as -// colorized with color.Fprintf(). -func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) { - return func(w io.Writer, format string, a ...interface{}) { - c.Fprintf(w, format, a...) - } -} - -// PrintfFunc returns a new function that prints the passed arguments as -// colorized with color.Printf(). -func (c *Color) PrintfFunc() func(format string, a ...interface{}) { - return func(format string, a ...interface{}) { - c.Printf(format, a...) - } -} - -// FprintlnFunc returns a new function that prints the passed arguments as -// colorized with color.Fprintln(). -func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) { - return func(w io.Writer, a ...interface{}) { - c.Fprintln(w, a...) - } -} - -// PrintlnFunc returns a new function that prints the passed arguments as -// colorized with color.Println(). -func (c *Color) PrintlnFunc() func(a ...interface{}) { - return func(a ...interface{}) { - c.Println(a...) - } -} - -// SprintFunc returns a new function that returns colorized strings for the -// given arguments with fmt.Sprint(). Useful to put into or mix into other -// string. Windows users should use this in conjunction with color.Output, example: -// -// put := New(FgYellow).SprintFunc() -// fmt.Fprintf(color.Output, "This is a %s", put("warning")) -func (c *Color) SprintFunc() func(a ...interface{}) string { - return func(a ...interface{}) string { - return c.wrap(fmt.Sprint(a...)) - } -} - -// SprintfFunc returns a new function that returns colorized strings for the -// given arguments with fmt.Sprintf(). Useful to put into or mix into other -// string. Windows users should use this in conjunction with color.Output. -func (c *Color) SprintfFunc() func(format string, a ...interface{}) string { - return func(format string, a ...interface{}) string { - return c.wrap(fmt.Sprintf(format, a...)) - } -} - -// SprintlnFunc returns a new function that returns colorized strings for the -// given arguments with fmt.Sprintln(). Useful to put into or mix into other -// string. Windows users should use this in conjunction with color.Output. -func (c *Color) SprintlnFunc() func(a ...interface{}) string { - return func(a ...interface{}) string { - return c.wrap(fmt.Sprintln(a...)) - } -} - -// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m" -// an example output might be: "1;36" -> bold cyan -func (c *Color) sequence() string { - format := make([]string, len(c.params)) - for i, v := range c.params { - format[i] = strconv.Itoa(int(v)) - } - - return strings.Join(format, ";") -} - -// wrap wraps the s string with the colors attributes. The string is ready to -// be printed. -func (c *Color) wrap(s string) string { - if c.isNoColorSet() { - return s - } - - return c.format() + s + c.unformat() -} - -func (c *Color) format() string { - return fmt.Sprintf("%s[%sm", escape, c.sequence()) -} - -func (c *Color) unformat() string { - return fmt.Sprintf("%s[%dm", escape, Reset) -} - -// DisableColor disables the color output. Useful to not change any existing -// code and still being able to output. Can be used for flags like -// "--no-color". To enable back use EnableColor() method. -func (c *Color) DisableColor() { - c.noColor = boolPtr(true) -} - -// EnableColor enables the color output. Use it in conjunction with -// DisableColor(). Otherwise this method has no side effects. -func (c *Color) EnableColor() { - c.noColor = boolPtr(false) -} - -func (c *Color) isNoColorSet() bool { - // check first if we have user setted action - if c.noColor != nil { - return *c.noColor - } - - // if not return the global option, which is disabled by default - return NoColor -} - -// Equals returns a boolean value indicating whether two colors are equal. -func (c *Color) Equals(c2 *Color) bool { - if len(c.params) != len(c2.params) { - return false - } - - for _, attr := range c.params { - if !c2.attrExists(attr) { - return false - } - } - - return true -} - -func (c *Color) attrExists(a Attribute) bool { - for _, attr := range c.params { - if attr == a { - return true - } - } - - return false -} - -func boolPtr(v bool) *bool { - return &v -} - -func getCachedColor(p Attribute) *Color { - colorsCacheMu.Lock() - defer colorsCacheMu.Unlock() - - c, ok := colorsCache[p] - if !ok { - c = New(p) - colorsCache[p] = c - } - - return c -} - -func colorPrint(format string, p Attribute, a ...interface{}) { - c := getCachedColor(p) - - if !strings.HasSuffix(format, "\n") { - format += "\n" - } - - if len(a) == 0 { - c.Print(format) - } else { - c.Printf(format, a...) - } -} - -func colorString(format string, p Attribute, a ...interface{}) string { - c := getCachedColor(p) - - if len(a) == 0 { - return c.SprintFunc()(format) - } - - return c.SprintfFunc()(format, a...) -} - -// Black is a convenient helper function to print with black foreground. A -// newline is appended to format by default. -func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) } - -// Red is a convenient helper function to print with red foreground. A -// newline is appended to format by default. -func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) } - -// Green is a convenient helper function to print with green foreground. A -// newline is appended to format by default. -func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) } - -// Yellow is a convenient helper function to print with yellow foreground. -// A newline is appended to format by default. -func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) } - -// Blue is a convenient helper function to print with blue foreground. A -// newline is appended to format by default. -func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) } - -// Magenta is a convenient helper function to print with magenta foreground. -// A newline is appended to format by default. -func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) } - -// Cyan is a convenient helper function to print with cyan foreground. A -// newline is appended to format by default. -func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) } - -// White is a convenient helper function to print with white foreground. A -// newline is appended to format by default. -func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) } - -// BlackString is a convenient helper function to return a string with black -// foreground. -func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) } - -// RedString is a convenient helper function to return a string with red -// foreground. -func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) } - -// GreenString is a convenient helper function to return a string with green -// foreground. -func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) } - -// YellowString is a convenient helper function to return a string with yellow -// foreground. -func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) } - -// BlueString is a convenient helper function to return a string with blue -// foreground. -func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) } - -// MagentaString is a convenient helper function to return a string with magenta -// foreground. -func MagentaString(format string, a ...interface{}) string { - return colorString(format, FgMagenta, a...) -} - -// CyanString is a convenient helper function to return a string with cyan -// foreground. -func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) } - -// WhiteString is a convenient helper function to return a string with white -// foreground. -func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) } - -// HiBlack is a convenient helper function to print with hi-intensity black foreground. A -// newline is appended to format by default. -func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) } - -// HiRed is a convenient helper function to print with hi-intensity red foreground. A -// newline is appended to format by default. -func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) } - -// HiGreen is a convenient helper function to print with hi-intensity green foreground. A -// newline is appended to format by default. -func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) } - -// HiYellow is a convenient helper function to print with hi-intensity yellow foreground. -// A newline is appended to format by default. -func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) } - -// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A -// newline is appended to format by default. -func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) } - -// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground. -// A newline is appended to format by default. -func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) } - -// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A -// newline is appended to format by default. -func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) } - -// HiWhite is a convenient helper function to print with hi-intensity white foreground. A -// newline is appended to format by default. -func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) } - -// HiBlackString is a convenient helper function to return a string with hi-intensity black -// foreground. -func HiBlackString(format string, a ...interface{}) string { - return colorString(format, FgHiBlack, a...) -} - -// HiRedString is a convenient helper function to return a string with hi-intensity red -// foreground. -func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) } - -// HiGreenString is a convenient helper function to return a string with hi-intensity green -// foreground. -func HiGreenString(format string, a ...interface{}) string { - return colorString(format, FgHiGreen, a...) -} - -// HiYellowString is a convenient helper function to return a string with hi-intensity yellow -// foreground. -func HiYellowString(format string, a ...interface{}) string { - return colorString(format, FgHiYellow, a...) -} - -// HiBlueString is a convenient helper function to return a string with hi-intensity blue -// foreground. -func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) } - -// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta -// foreground. -func HiMagentaString(format string, a ...interface{}) string { - return colorString(format, FgHiMagenta, a...) -} - -// HiCyanString is a convenient helper function to return a string with hi-intensity cyan -// foreground. -func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) } - -// HiWhiteString is a convenient helper function to return a string with hi-intensity white -// foreground. -func HiWhiteString(format string, a ...interface{}) string { - return colorString(format, FgHiWhite, a...) -} diff --git a/vendor/github.com/fatih/color/doc.go b/vendor/github.com/fatih/color/doc.go deleted file mode 100644 index cf1e9650..00000000 --- a/vendor/github.com/fatih/color/doc.go +++ /dev/null @@ -1,133 +0,0 @@ -/* -Package color is an ANSI color package to output colorized or SGR defined -output to the standard output. The API can be used in several way, pick one -that suits you. - -Use simple and default helper functions with predefined foreground colors: - - color.Cyan("Prints text in cyan.") - - // a newline will be appended automatically - color.Blue("Prints %s in blue.", "text") - - // More default foreground colors.. - color.Red("We have red") - color.Yellow("Yellow color too!") - color.Magenta("And many others ..") - - // Hi-intensity colors - color.HiGreen("Bright green color.") - color.HiBlack("Bright black means gray..") - color.HiWhite("Shiny white color!") - -However there are times where custom color mixes are required. Below are some -examples to create custom color objects and use the print functions of each -separate color object. - - // Create a new color object - c := color.New(color.FgCyan).Add(color.Underline) - c.Println("Prints cyan text with an underline.") - - // Or just add them to New() - d := color.New(color.FgCyan, color.Bold) - d.Printf("This prints bold cyan %s\n", "too!.") - - - // Mix up foreground and background colors, create new mixes! - red := color.New(color.FgRed) - - boldRed := red.Add(color.Bold) - boldRed.Println("This will print text in bold red.") - - whiteBackground := red.Add(color.BgWhite) - whiteBackground.Println("Red text with White background.") - - // Use your own io.Writer output - color.New(color.FgBlue).Fprintln(myWriter, "blue color!") - - blue := color.New(color.FgBlue) - blue.Fprint(myWriter, "This will print text in blue.") - -You can create PrintXxx functions to simplify even more: - - // Create a custom print function for convenient - red := color.New(color.FgRed).PrintfFunc() - red("warning") - red("error: %s", err) - - // Mix up multiple attributes - notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() - notice("don't forget this...") - -You can also FprintXxx functions to pass your own io.Writer: - - blue := color.New(FgBlue).FprintfFunc() - blue(myWriter, "important notice: %s", stars) - - // Mix up with multiple attributes - success := color.New(color.Bold, color.FgGreen).FprintlnFunc() - success(myWriter, don't forget this...") - - -Or create SprintXxx functions to mix strings with other non-colorized strings: - - yellow := New(FgYellow).SprintFunc() - red := New(FgRed).SprintFunc() - - fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) - - info := New(FgWhite, BgGreen).SprintFunc() - fmt.Printf("this %s rocks!\n", info("package")) - -Windows support is enabled by default. All Print functions work as intended. -However only for color.SprintXXX functions, user should use fmt.FprintXXX and -set the output to color.Output: - - fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) - - info := New(FgWhite, BgGreen).SprintFunc() - fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) - -Using with existing code is possible. Just use the Set() method to set the -standard output to the given parameters. That way a rewrite of an existing -code is not required. - - // Use handy standard colors. - color.Set(color.FgYellow) - - fmt.Println("Existing text will be now in Yellow") - fmt.Printf("This one %s\n", "too") - - color.Unset() // don't forget to unset - - // You can mix up parameters - color.Set(color.FgMagenta, color.Bold) - defer color.Unset() // use it in your function - - fmt.Println("All text will be now bold magenta.") - -There might be a case where you want to disable color output (for example to -pipe the standard output of your app to somewhere else). `Color` has support to -disable colors both globally and for single color definition. For example -suppose you have a CLI app and a `--no-color` bool flag. You can easily disable -the color output with: - - var flagNoColor = flag.Bool("no-color", false, "Disable color output") - - if *flagNoColor { - color.NoColor = true // disables colorized output - } - -It also has support for single color definitions (local). You can -disable/enable color output on the fly: - - c := color.New(color.FgCyan) - c.Println("Prints cyan text") - - c.DisableColor() - c.Println("This is printed without any color") - - c.EnableColor() - c.Println("This prints again cyan...") -*/ -package color diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go new file mode 100644 index 00000000..41bbddc6 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go @@ -0,0 +1,89 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package cmpopts provides common options for the cmp package. +package cmpopts + +import ( + "math" + "reflect" + + "github.com/google/go-cmp/cmp" +) + +func equateAlways(_, _ interface{}) bool { return true } + +// EquateEmpty returns a Comparer option that determines all maps and slices +// with a length of zero to be equal, regardless of whether they are nil. +// +// EquateEmpty can be used in conjunction with SortSlices and SortMaps. +func EquateEmpty() cmp.Option { + return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) +} + +func isEmpty(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) +} + +// EquateApprox returns a Comparer option that determines float32 or float64 +// values to be equal if they are within a relative fraction or absolute margin. +// This option is not used when either x or y is NaN or infinite. +// +// The fraction determines that the difference of two values must be within the +// smaller fraction of the two values, while the margin determines that the two +// values must be within some absolute margin. +// To express only a fraction or only a margin, use 0 for the other parameter. +// The fraction and margin must be non-negative. +// +// The mathematical expression used is equivalent to: +// |x-y| ≤ max(fraction*min(|x|, |y|), margin) +// +// EquateApprox can be used in conjunction with EquateNaNs. +func EquateApprox(fraction, margin float64) cmp.Option { + if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { + panic("margin or fraction must be a non-negative number") + } + a := approximator{fraction, margin} + return cmp.Options{ + cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)), + cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)), + } +} + +type approximator struct{ frac, marg float64 } + +func areRealF64s(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) +} +func areRealF32s(x, y float32) bool { + return areRealF64s(float64(x), float64(y)) +} +func (a approximator) compareF64(x, y float64) bool { + relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(a.marg, relMarg) +} +func (a approximator) compareF32(x, y float32) bool { + return a.compareF64(float64(x), float64(y)) +} + +// EquateNaNs returns a Comparer option that determines float32 and float64 +// NaN values to be equal. +// +// EquateNaNs can be used in conjunction with EquateApprox. +func EquateNaNs() cmp.Option { + return cmp.Options{ + cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), + cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)), + } +} + +func areNaNsF64s(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) +} +func areNaNsF32s(x, y float32) bool { + return areNaNsF64s(float64(x), float64(y)) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go new file mode 100644 index 00000000..e86554b9 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go @@ -0,0 +1,145 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" +) + +// IgnoreFields returns an Option that ignores exported fields of the +// given names on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a +// specific sub-field that is embedded or nested within the parent struct. +// +// This does not handle unexported fields; use IgnoreUnexported instead. +func IgnoreFields(typ interface{}, names ...string) cmp.Option { + sf := newStructFilter(typ, names...) + return cmp.FilterPath(sf.filter, cmp.Ignore()) +} + +// IgnoreTypes returns an Option that ignores all values assignable to +// certain types, which are specified by passing in a value of each type. +func IgnoreTypes(typs ...interface{}) cmp.Option { + tf := newTypeFilter(typs...) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type typeFilter []reflect.Type + +func newTypeFilter(typs ...interface{}) (tf typeFilter) { + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil { + // This occurs if someone tries to pass in sync.Locker(nil) + panic("cannot determine type; consider using IgnoreInterfaces") + } + tf = append(tf, t) + } + return tf +} +func (tf typeFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreInterfaces returns an Option that ignores all values or references of +// values assignable to certain interface types. These interfaces are specified +// by passing in an anonymous struct with the interface types embedded in it. +// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}. +func IgnoreInterfaces(ifaces interface{}) cmp.Option { + tf := newIfaceFilter(ifaces) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type ifaceFilter []reflect.Type + +func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { + t := reflect.TypeOf(ifaces) + if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct { + panic("input must be an anonymous struct") + } + for i := 0; i < t.NumField(); i++ { + fi := t.Field(i) + switch { + case !fi.Anonymous: + panic("struct cannot have named fields") + case fi.Type.Kind() != reflect.Interface: + panic("embedded field must be an interface type") + case fi.Type.NumMethod() == 0: + // This matches everything; why would you ever want this? + panic("cannot ignore empty interface") + default: + tf = append(tf, fi.Type) + } + } + return tf +} +func (tf ifaceFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreUnexported returns an Option that only ignores the immediate unexported +// fields of a struct, including anonymous fields of unexported types. +// In particular, unexported fields within the struct's exported fields +// of struct types, including anonymous fields, will not be ignored unless the +// type of the field itself is also passed to IgnoreUnexported. +func IgnoreUnexported(typs ...interface{}) cmp.Option { + ux := newUnexportedFilter(typs...) + return cmp.FilterPath(ux.filter, cmp.Ignore()) +} + +type unexportedFilter struct{ m map[reflect.Type]bool } + +func newUnexportedFilter(typs ...interface{}) unexportedFilter { + ux := unexportedFilter{m: make(map[reflect.Type]bool)} + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + ux.m[t] = true + } + return ux +} +func (xf unexportedFilter) filter(p cmp.Path) bool { + sf, ok := p.Index(-1).(cmp.StructField) + if !ok { + return false + } + return xf.m[p.Index(-2).Type()] && !isExported(sf.Name()) +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go new file mode 100644 index 00000000..da17d746 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go @@ -0,0 +1,146 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// SortSlices returns a Transformer option that sorts all []V. +// The less function must be of the form "func(T, T) bool" which is used to +// sort any slice with element type V that is assignable to T. +// +// The less function must be: +// • Deterministic: less(x, y) == less(x, y) +// • Irreflexive: !less(x, x) +// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// +// The less function does not have to be "total". That is, if !less(x, y) and +// !less(y, x) for two elements x and y, their relative order is maintained. +// +// SortSlices can be used in conjunction with EquateEmpty. +func SortSlices(less interface{}) cmp.Option { + vf := reflect.ValueOf(less) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", less)) + } + ss := sliceSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ss.filter, cmp.Transformer("Sort", ss.sort)) +} + +type sliceSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ss sliceSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + if !(x != nil && y != nil && vx.Type() == vy.Type()) || + !(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) || + (vx.Len() <= 1 && vy.Len() <= 1) { + return false + } + // Check whether the slices are already sorted to avoid an infinite + // recursion cycle applying the same transform to itself. + ok1 := sliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) }) + ok2 := sliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) }) + return !ok1 || !ok2 +} +func (ss sliceSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len()) + for i := 0; i < src.Len(); i++ { + dst.Index(i).Set(src.Index(i)) + } + sortSliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) }) + ss.checkSort(dst) + return dst.Interface() +} +func (ss sliceSorter) checkSort(v reflect.Value) { + start := -1 // Start of a sequence of equal elements. + for i := 1; i < v.Len(); i++ { + if ss.less(v, i-1, i) { + // Check that first and last elements in v[start:i] are equal. + if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) { + panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i))) + } + start = -1 + } else if start == -1 { + start = i + } + } +} +func (ss sliceSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i), v.Index(j) + return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} + +// SortMaps returns a Transformer option that flattens map[K]V types to be a +// sorted []struct{K, V}. The less function must be of the form +// "func(T, T) bool" which is used to sort any map with key K that is +// assignable to T. +// +// Flattening the map into a slice has the property that cmp.Equal is able to +// use Comparers on K or the K.Equal method if it exists. +// +// The less function must be: +// • Deterministic: less(x, y) == less(x, y) +// • Irreflexive: !less(x, x) +// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// • Total: if x != y, then either less(x, y) or less(y, x) +// +// SortMaps can be used in conjunction with EquateEmpty. +func SortMaps(less interface{}) cmp.Option { + vf := reflect.ValueOf(less) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", less)) + } + ms := mapSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ms.filter, cmp.Transformer("Sort", ms.sort)) +} + +type mapSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ms mapSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) && + (vx.Len() != 0 || vy.Len() != 0) +} +func (ms mapSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + outType := mapEntryType(src.Type()) + dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len()) + for i, k := range src.MapKeys() { + v := reflect.New(outType).Elem() + v.Field(0).Set(k) + v.Field(1).Set(src.MapIndex(k)) + dst.Index(i).Set(v) + } + sortSlice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) }) + ms.checkSort(dst) + return dst.Interface() +} +func (ms mapSorter) checkSort(v reflect.Value) { + for i := 1; i < v.Len(); i++ { + if !ms.less(v, i-1, i) { + panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i))) + } + } +} +func (ms mapSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i).Field(0), v.Index(j).Field(0) + if !hasReflectStructOf { + vx, vy = vx.Elem(), vy.Elem() + } + return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go new file mode 100644 index 00000000..839b88ca --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go @@ -0,0 +1,46 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build !go1.8 + +package cmpopts + +import ( + "reflect" + "sort" +) + +const hasReflectStructOf = false + +func mapEntryType(reflect.Type) reflect.Type { + return reflect.TypeOf(struct{ K, V interface{} }{}) +} + +func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool { + return sort.IsSorted(reflectSliceSorter{reflect.ValueOf(slice), less}) +} +func sortSlice(slice interface{}, less func(i, j int) bool) { + sort.Sort(reflectSliceSorter{reflect.ValueOf(slice), less}) +} +func sortSliceStable(slice interface{}, less func(i, j int) bool) { + sort.Stable(reflectSliceSorter{reflect.ValueOf(slice), less}) +} + +type reflectSliceSorter struct { + slice reflect.Value + less func(i, j int) bool +} + +func (ss reflectSliceSorter) Len() int { + return ss.slice.Len() +} +func (ss reflectSliceSorter) Less(i, j int) bool { + return ss.less(i, j) +} +func (ss reflectSliceSorter) Swap(i, j int) { + vi := ss.slice.Index(i).Interface() + vj := ss.slice.Index(j).Interface() + ss.slice.Index(i).Set(reflect.ValueOf(vj)) + ss.slice.Index(j).Set(reflect.ValueOf(vi)) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go new file mode 100644 index 00000000..8a59c0d3 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go @@ -0,0 +1,31 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build go1.8 + +package cmpopts + +import ( + "reflect" + "sort" +) + +const hasReflectStructOf = true + +func mapEntryType(t reflect.Type) reflect.Type { + return reflect.StructOf([]reflect.StructField{ + {Name: "K", Type: t.Key()}, + {Name: "V", Type: t.Elem()}, + }) +} + +func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool { + return sort.SliceIsSorted(slice, less) +} +func sortSlice(slice interface{}, less func(i, j int) bool) { + sort.Slice(slice, less) +} +func sortSliceStable(slice interface{}, less func(i, j int) bool) { + sort.SliceStable(slice, less) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go new file mode 100644 index 00000000..97f70798 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go @@ -0,0 +1,182 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// filterField returns a new Option where opt is only evaluated on paths that +// include a specific exported field on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a +// specific sub-field that is embedded or nested within the parent struct. +func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option { + // TODO: This is currently unexported over concerns of how helper filters + // can be composed together easily. + // TODO: Add tests for FilterField. + + sf := newStructFilter(typ, name) + return cmp.FilterPath(sf.filter, opt) +} + +type structFilter struct { + t reflect.Type // The root struct type to match on + ft fieldTree // Tree of fields to match on +} + +func newStructFilter(typ interface{}, names ...string) structFilter { + // TODO: Perhaps allow * as a special identifier to allow ignoring any + // number of path steps until the next field match? + // This could be useful when a concrete struct gets transformed into + // an anonymous struct where it is not possible to specify that by type, + // but the transformer happens to provide guarantees about the names of + // the transformed fields. + + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a struct", typ)) + } + var ft fieldTree + for _, name := range names { + cname, err := canonicalName(t, name) + if err != nil { + panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err)) + } + ft.insert(cname) + } + return structFilter{t, ft} +} + +func (sf structFilter) filter(p cmp.Path) bool { + for i, ps := range p { + if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) { + return true + } + } + return false +} + +// fieldTree represents a set of dot-separated identifiers. +// +// For example, inserting the following selectors: +// Foo +// Foo.Bar.Baz +// Foo.Buzz +// Nuka.Cola.Quantum +// +// Results in a tree of the form: +// {sub: { +// "Foo": {ok: true, sub: { +// "Bar": {sub: { +// "Baz": {ok: true}, +// }}, +// "Buzz": {ok: true}, +// }}, +// "Nuka": {sub: { +// "Cola": {sub: { +// "Quantum": {ok: true}, +// }}, +// }}, +// }} +type fieldTree struct { + ok bool // Whether this is a specified node + sub map[string]fieldTree // The sub-tree of fields under this node +} + +// insert inserts a sequence of field accesses into the tree. +func (ft *fieldTree) insert(cname []string) { + if ft.sub == nil { + ft.sub = make(map[string]fieldTree) + } + if len(cname) == 0 { + ft.ok = true + return + } + sub := ft.sub[cname[0]] + sub.insert(cname[1:]) + ft.sub[cname[0]] = sub +} + +// matchPrefix reports whether any selector in the fieldTree matches +// the start of path p. +func (ft fieldTree) matchPrefix(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + ft = ft.sub[ps.Name()] + if ft.ok { + return true + } + if len(ft.sub) == 0 { + return false + } + case cmp.Indirect: + default: + return false + } + } + return false +} + +// canonicalName returns a list of identifiers where any struct field access +// through an embedded field is expanded to include the names of the embedded +// types themselves. +// +// For example, suppose field "Foo" is not directly in the parent struct, +// but actually from an embedded struct of type "Bar". Then, the canonical name +// of "Foo" is actually "Bar.Foo". +// +// Suppose field "Foo" is not directly in the parent struct, but actually +// a field in two different embedded structs of types "Bar" and "Baz". +// Then the selector "Foo" causes a panic since it is ambiguous which one it +// refers to. The user must specify either "Bar.Foo" or "Baz.Foo". +func canonicalName(t reflect.Type, sel string) ([]string, error) { + var name string + sel = strings.TrimPrefix(sel, ".") + if sel == "" { + return nil, fmt.Errorf("name must not be empty") + } + if i := strings.IndexByte(sel, '.'); i < 0 { + name, sel = sel, "" + } else { + name, sel = sel[:i], sel[i:] + } + + // Type must be a struct or pointer to struct. + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("%v must be a struct", t) + } + + // Find the canonical name for this current field name. + // If the field exists in an embedded struct, then it will be expanded. + if !isExported(name) { + // Disallow unexported fields: + // * To discourage people from actually touching unexported fields + // * FieldByName is buggy (https://golang.org/issue/4876) + return []string{name}, fmt.Errorf("name must be exported") + } + sf, ok := t.FieldByName(name) + if !ok { + return []string{name}, fmt.Errorf("does not exist") + } + var ss []string + for i := range sf.Index { + ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name) + } + if sel == "" { + return ss, nil + } + ssPost, err := canonicalName(sf.Type, sel) + return append(ss, ssPost...), err +} diff --git a/vendor/github.com/mattn/go-colorable/LICENSE b/vendor/github.com/mattn/go-colorable/LICENSE deleted file mode 100644 index 91b5cef3..00000000 --- a/vendor/github.com/mattn/go-colorable/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Yasuhiro Matsumoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/mattn/go-colorable/colorable_appengine.go b/vendor/github.com/mattn/go-colorable/colorable_appengine.go deleted file mode 100644 index 1f28d773..00000000 --- a/vendor/github.com/mattn/go-colorable/colorable_appengine.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build appengine - -package colorable - -import ( - "io" - "os" - - _ "github.com/mattn/go-isatty" -) - -// NewColorable return new instance of Writer which handle escape sequence. -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - return file -} - -// NewColorableStdout return new instance of Writer which handle escape sequence for stdout. -func NewColorableStdout() io.Writer { - return os.Stdout -} - -// NewColorableStderr return new instance of Writer which handle escape sequence for stderr. -func NewColorableStderr() io.Writer { - return os.Stderr -} diff --git a/vendor/github.com/mattn/go-colorable/colorable_others.go b/vendor/github.com/mattn/go-colorable/colorable_others.go deleted file mode 100644 index 887f203d..00000000 --- a/vendor/github.com/mattn/go-colorable/colorable_others.go +++ /dev/null @@ -1,30 +0,0 @@ -// +build !windows -// +build !appengine - -package colorable - -import ( - "io" - "os" - - _ "github.com/mattn/go-isatty" -) - -// NewColorable return new instance of Writer which handle escape sequence. -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - return file -} - -// NewColorableStdout return new instance of Writer which handle escape sequence for stdout. -func NewColorableStdout() io.Writer { - return os.Stdout -} - -// NewColorableStderr return new instance of Writer which handle escape sequence for stderr. -func NewColorableStderr() io.Writer { - return os.Stderr -} diff --git a/vendor/github.com/mattn/go-colorable/colorable_windows.go b/vendor/github.com/mattn/go-colorable/colorable_windows.go deleted file mode 100644 index e17a5474..00000000 --- a/vendor/github.com/mattn/go-colorable/colorable_windows.go +++ /dev/null @@ -1,884 +0,0 @@ -// +build windows -// +build !appengine - -package colorable - -import ( - "bytes" - "io" - "math" - "os" - "strconv" - "strings" - "syscall" - "unsafe" - - "github.com/mattn/go-isatty" -) - -const ( - foregroundBlue = 0x1 - foregroundGreen = 0x2 - foregroundRed = 0x4 - foregroundIntensity = 0x8 - foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) - backgroundBlue = 0x10 - backgroundGreen = 0x20 - backgroundRed = 0x40 - backgroundIntensity = 0x80 - backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) -) - -type wchar uint16 -type short int16 -type dword uint32 -type word uint16 - -type coord struct { - x short - y short -} - -type smallRect struct { - left short - top short - right short - bottom short -} - -type consoleScreenBufferInfo struct { - size coord - cursorPosition coord - attributes word - window smallRect - maximumWindowSize coord -} - -type consoleCursorInfo struct { - size dword - visible int32 -} - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") - procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") - procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") - procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") - procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") - procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") - procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") - procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW") -) - -// Writer provide colorable Writer to the console -type Writer struct { - out io.Writer - handle syscall.Handle - oldattr word - oldpos coord -} - -// NewColorable return new instance of Writer which handle escape sequence from File. -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - if isatty.IsTerminal(file.Fd()) { - var csbi consoleScreenBufferInfo - handle := syscall.Handle(file.Fd()) - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}} - } - return file -} - -// NewColorableStdout return new instance of Writer which handle escape sequence for stdout. -func NewColorableStdout() io.Writer { - return NewColorable(os.Stdout) -} - -// NewColorableStderr return new instance of Writer which handle escape sequence for stderr. -func NewColorableStderr() io.Writer { - return NewColorable(os.Stderr) -} - -var color256 = map[int]int{ - 0: 0x000000, - 1: 0x800000, - 2: 0x008000, - 3: 0x808000, - 4: 0x000080, - 5: 0x800080, - 6: 0x008080, - 7: 0xc0c0c0, - 8: 0x808080, - 9: 0xff0000, - 10: 0x00ff00, - 11: 0xffff00, - 12: 0x0000ff, - 13: 0xff00ff, - 14: 0x00ffff, - 15: 0xffffff, - 16: 0x000000, - 17: 0x00005f, - 18: 0x000087, - 19: 0x0000af, - 20: 0x0000d7, - 21: 0x0000ff, - 22: 0x005f00, - 23: 0x005f5f, - 24: 0x005f87, - 25: 0x005faf, - 26: 0x005fd7, - 27: 0x005fff, - 28: 0x008700, - 29: 0x00875f, - 30: 0x008787, - 31: 0x0087af, - 32: 0x0087d7, - 33: 0x0087ff, - 34: 0x00af00, - 35: 0x00af5f, - 36: 0x00af87, - 37: 0x00afaf, - 38: 0x00afd7, - 39: 0x00afff, - 40: 0x00d700, - 41: 0x00d75f, - 42: 0x00d787, - 43: 0x00d7af, - 44: 0x00d7d7, - 45: 0x00d7ff, - 46: 0x00ff00, - 47: 0x00ff5f, - 48: 0x00ff87, - 49: 0x00ffaf, - 50: 0x00ffd7, - 51: 0x00ffff, - 52: 0x5f0000, - 53: 0x5f005f, - 54: 0x5f0087, - 55: 0x5f00af, - 56: 0x5f00d7, - 57: 0x5f00ff, - 58: 0x5f5f00, - 59: 0x5f5f5f, - 60: 0x5f5f87, - 61: 0x5f5faf, - 62: 0x5f5fd7, - 63: 0x5f5fff, - 64: 0x5f8700, - 65: 0x5f875f, - 66: 0x5f8787, - 67: 0x5f87af, - 68: 0x5f87d7, - 69: 0x5f87ff, - 70: 0x5faf00, - 71: 0x5faf5f, - 72: 0x5faf87, - 73: 0x5fafaf, - 74: 0x5fafd7, - 75: 0x5fafff, - 76: 0x5fd700, - 77: 0x5fd75f, - 78: 0x5fd787, - 79: 0x5fd7af, - 80: 0x5fd7d7, - 81: 0x5fd7ff, - 82: 0x5fff00, - 83: 0x5fff5f, - 84: 0x5fff87, - 85: 0x5fffaf, - 86: 0x5fffd7, - 87: 0x5fffff, - 88: 0x870000, - 89: 0x87005f, - 90: 0x870087, - 91: 0x8700af, - 92: 0x8700d7, - 93: 0x8700ff, - 94: 0x875f00, - 95: 0x875f5f, - 96: 0x875f87, - 97: 0x875faf, - 98: 0x875fd7, - 99: 0x875fff, - 100: 0x878700, - 101: 0x87875f, - 102: 0x878787, - 103: 0x8787af, - 104: 0x8787d7, - 105: 0x8787ff, - 106: 0x87af00, - 107: 0x87af5f, - 108: 0x87af87, - 109: 0x87afaf, - 110: 0x87afd7, - 111: 0x87afff, - 112: 0x87d700, - 113: 0x87d75f, - 114: 0x87d787, - 115: 0x87d7af, - 116: 0x87d7d7, - 117: 0x87d7ff, - 118: 0x87ff00, - 119: 0x87ff5f, - 120: 0x87ff87, - 121: 0x87ffaf, - 122: 0x87ffd7, - 123: 0x87ffff, - 124: 0xaf0000, - 125: 0xaf005f, - 126: 0xaf0087, - 127: 0xaf00af, - 128: 0xaf00d7, - 129: 0xaf00ff, - 130: 0xaf5f00, - 131: 0xaf5f5f, - 132: 0xaf5f87, - 133: 0xaf5faf, - 134: 0xaf5fd7, - 135: 0xaf5fff, - 136: 0xaf8700, - 137: 0xaf875f, - 138: 0xaf8787, - 139: 0xaf87af, - 140: 0xaf87d7, - 141: 0xaf87ff, - 142: 0xafaf00, - 143: 0xafaf5f, - 144: 0xafaf87, - 145: 0xafafaf, - 146: 0xafafd7, - 147: 0xafafff, - 148: 0xafd700, - 149: 0xafd75f, - 150: 0xafd787, - 151: 0xafd7af, - 152: 0xafd7d7, - 153: 0xafd7ff, - 154: 0xafff00, - 155: 0xafff5f, - 156: 0xafff87, - 157: 0xafffaf, - 158: 0xafffd7, - 159: 0xafffff, - 160: 0xd70000, - 161: 0xd7005f, - 162: 0xd70087, - 163: 0xd700af, - 164: 0xd700d7, - 165: 0xd700ff, - 166: 0xd75f00, - 167: 0xd75f5f, - 168: 0xd75f87, - 169: 0xd75faf, - 170: 0xd75fd7, - 171: 0xd75fff, - 172: 0xd78700, - 173: 0xd7875f, - 174: 0xd78787, - 175: 0xd787af, - 176: 0xd787d7, - 177: 0xd787ff, - 178: 0xd7af00, - 179: 0xd7af5f, - 180: 0xd7af87, - 181: 0xd7afaf, - 182: 0xd7afd7, - 183: 0xd7afff, - 184: 0xd7d700, - 185: 0xd7d75f, - 186: 0xd7d787, - 187: 0xd7d7af, - 188: 0xd7d7d7, - 189: 0xd7d7ff, - 190: 0xd7ff00, - 191: 0xd7ff5f, - 192: 0xd7ff87, - 193: 0xd7ffaf, - 194: 0xd7ffd7, - 195: 0xd7ffff, - 196: 0xff0000, - 197: 0xff005f, - 198: 0xff0087, - 199: 0xff00af, - 200: 0xff00d7, - 201: 0xff00ff, - 202: 0xff5f00, - 203: 0xff5f5f, - 204: 0xff5f87, - 205: 0xff5faf, - 206: 0xff5fd7, - 207: 0xff5fff, - 208: 0xff8700, - 209: 0xff875f, - 210: 0xff8787, - 211: 0xff87af, - 212: 0xff87d7, - 213: 0xff87ff, - 214: 0xffaf00, - 215: 0xffaf5f, - 216: 0xffaf87, - 217: 0xffafaf, - 218: 0xffafd7, - 219: 0xffafff, - 220: 0xffd700, - 221: 0xffd75f, - 222: 0xffd787, - 223: 0xffd7af, - 224: 0xffd7d7, - 225: 0xffd7ff, - 226: 0xffff00, - 227: 0xffff5f, - 228: 0xffff87, - 229: 0xffffaf, - 230: 0xffffd7, - 231: 0xffffff, - 232: 0x080808, - 233: 0x121212, - 234: 0x1c1c1c, - 235: 0x262626, - 236: 0x303030, - 237: 0x3a3a3a, - 238: 0x444444, - 239: 0x4e4e4e, - 240: 0x585858, - 241: 0x626262, - 242: 0x6c6c6c, - 243: 0x767676, - 244: 0x808080, - 245: 0x8a8a8a, - 246: 0x949494, - 247: 0x9e9e9e, - 248: 0xa8a8a8, - 249: 0xb2b2b2, - 250: 0xbcbcbc, - 251: 0xc6c6c6, - 252: 0xd0d0d0, - 253: 0xdadada, - 254: 0xe4e4e4, - 255: 0xeeeeee, -} - -// `\033]0;TITLESTR\007` -func doTitleSequence(er *bytes.Reader) error { - var c byte - var err error - - c, err = er.ReadByte() - if err != nil { - return err - } - if c != '0' && c != '2' { - return nil - } - c, err = er.ReadByte() - if err != nil { - return err - } - if c != ';' { - return nil - } - title := make([]byte, 0, 80) - for { - c, err = er.ReadByte() - if err != nil { - return err - } - if c == 0x07 || c == '\n' { - break - } - title = append(title, c) - } - if len(title) > 0 { - title8, err := syscall.UTF16PtrFromString(string(title)) - if err == nil { - procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8))) - } - } - return nil -} - -// Write write data on console -func (w *Writer) Write(data []byte) (n int, err error) { - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - - er := bytes.NewReader(data) - var bw [1]byte -loop: - for { - c1, err := er.ReadByte() - if err != nil { - break loop - } - if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) - continue - } - c2, err := er.ReadByte() - if err != nil { - break loop - } - - if c2 == ']' { - if err := doTitleSequence(er); err != nil { - break loop - } - continue - } - if c2 != 0x5b { - continue - } - - var buf bytes.Buffer - var m byte - for { - c, err := er.ReadByte() - if err != nil { - break loop - } - if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { - m = c - break - } - buf.Write([]byte(string(c))) - } - - switch m { - case 'A': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.y -= short(n) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'B': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.y += short(n) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'C': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x += short(n) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'D': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x -= short(n) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'E': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x = 0 - csbi.cursorPosition.y += short(n) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'F': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x = 0 - csbi.cursorPosition.y -= short(n) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'G': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x = short(n - 1) - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'H', 'f': - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - if buf.Len() > 0 { - token := strings.Split(buf.String(), ";") - switch len(token) { - case 1: - n1, err := strconv.Atoi(token[0]) - if err != nil { - continue - } - csbi.cursorPosition.y = short(n1 - 1) - case 2: - n1, err := strconv.Atoi(token[0]) - if err != nil { - continue - } - n2, err := strconv.Atoi(token[1]) - if err != nil { - continue - } - csbi.cursorPosition.x = short(n2 - 1) - csbi.cursorPosition.y = short(n1 - 1) - } - } else { - csbi.cursorPosition.y = 0 - } - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'J': - n := 0 - if buf.Len() > 0 { - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - } - var count, written dword - var cursor coord - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - switch n { - case 0: - cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} - count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x) - case 1: - cursor = coord{x: csbi.window.left, y: csbi.window.top} - count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.window.top-csbi.cursorPosition.y)*csbi.size.x) - case 2: - cursor = coord{x: csbi.window.left, y: csbi.window.top} - count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x) - } - procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - case 'K': - n := 0 - if buf.Len() > 0 { - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - } - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - var cursor coord - var count, written dword - switch n { - case 0: - cursor = coord{x: csbi.cursorPosition.x + 1, y: csbi.cursorPosition.y} - count = dword(csbi.size.x - csbi.cursorPosition.x - 1) - case 1: - cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} - count = dword(csbi.size.x - csbi.cursorPosition.x) - case 2: - cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} - count = dword(csbi.size.x) - } - procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - case 'm': - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - attr := csbi.attributes - cs := buf.String() - if cs == "" { - procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.oldattr)) - continue - } - token := strings.Split(cs, ";") - for i := 0; i < len(token); i++ { - ns := token[i] - if n, err = strconv.Atoi(ns); err == nil { - switch { - case n == 0 || n == 100: - attr = w.oldattr - case 1 <= n && n <= 5: - attr |= foregroundIntensity - case n == 7: - attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) - case n == 22 || n == 25: - attr |= foregroundIntensity - case n == 27: - attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) - case 30 <= n && n <= 37: - attr &= backgroundMask - if (n-30)&1 != 0 { - attr |= foregroundRed - } - if (n-30)&2 != 0 { - attr |= foregroundGreen - } - if (n-30)&4 != 0 { - attr |= foregroundBlue - } - case n == 38: // set foreground color. - if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") { - if n256, err := strconv.Atoi(token[i+2]); err == nil { - if n256foreAttr == nil { - n256setup() - } - attr &= backgroundMask - attr |= n256foreAttr[n256] - i += 2 - } - } else { - attr = attr & (w.oldattr & backgroundMask) - } - case n == 39: // reset foreground color. - attr &= backgroundMask - attr |= w.oldattr & foregroundMask - case 40 <= n && n <= 47: - attr &= foregroundMask - if (n-40)&1 != 0 { - attr |= backgroundRed - } - if (n-40)&2 != 0 { - attr |= backgroundGreen - } - if (n-40)&4 != 0 { - attr |= backgroundBlue - } - case n == 48: // set background color. - if i < len(token)-2 && token[i+1] == "5" { - if n256, err := strconv.Atoi(token[i+2]); err == nil { - if n256backAttr == nil { - n256setup() - } - attr &= foregroundMask - attr |= n256backAttr[n256] - i += 2 - } - } else { - attr = attr & (w.oldattr & foregroundMask) - } - case n == 49: // reset foreground color. - attr &= foregroundMask - attr |= w.oldattr & backgroundMask - case 90 <= n && n <= 97: - attr = (attr & backgroundMask) - attr |= foregroundIntensity - if (n-90)&1 != 0 { - attr |= foregroundRed - } - if (n-90)&2 != 0 { - attr |= foregroundGreen - } - if (n-90)&4 != 0 { - attr |= foregroundBlue - } - case 100 <= n && n <= 107: - attr = (attr & foregroundMask) - attr |= backgroundIntensity - if (n-100)&1 != 0 { - attr |= backgroundRed - } - if (n-100)&2 != 0 { - attr |= backgroundGreen - } - if (n-100)&4 != 0 { - attr |= backgroundBlue - } - } - procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr)) - } - } - case 'h': - var ci consoleCursorInfo - cs := buf.String() - if cs == "5>" { - procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 0 - procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - } else if cs == "?25" { - procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 1 - procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - } - case 'l': - var ci consoleCursorInfo - cs := buf.String() - if cs == "5>" { - procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 1 - procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - } else if cs == "?25" { - procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 0 - procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci))) - } - case 's': - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - w.oldpos = csbi.cursorPosition - case 'u': - procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) - } - } - - return len(data), nil -} - -type consoleColor struct { - rgb int - red bool - green bool - blue bool - intensity bool -} - -func (c consoleColor) foregroundAttr() (attr word) { - if c.red { - attr |= foregroundRed - } - if c.green { - attr |= foregroundGreen - } - if c.blue { - attr |= foregroundBlue - } - if c.intensity { - attr |= foregroundIntensity - } - return -} - -func (c consoleColor) backgroundAttr() (attr word) { - if c.red { - attr |= backgroundRed - } - if c.green { - attr |= backgroundGreen - } - if c.blue { - attr |= backgroundBlue - } - if c.intensity { - attr |= backgroundIntensity - } - return -} - -var color16 = []consoleColor{ - {0x000000, false, false, false, false}, - {0x000080, false, false, true, false}, - {0x008000, false, true, false, false}, - {0x008080, false, true, true, false}, - {0x800000, true, false, false, false}, - {0x800080, true, false, true, false}, - {0x808000, true, true, false, false}, - {0xc0c0c0, true, true, true, false}, - {0x808080, false, false, false, true}, - {0x0000ff, false, false, true, true}, - {0x00ff00, false, true, false, true}, - {0x00ffff, false, true, true, true}, - {0xff0000, true, false, false, true}, - {0xff00ff, true, false, true, true}, - {0xffff00, true, true, false, true}, - {0xffffff, true, true, true, true}, -} - -type hsv struct { - h, s, v float32 -} - -func (a hsv) dist(b hsv) float32 { - dh := a.h - b.h - switch { - case dh > 0.5: - dh = 1 - dh - case dh < -0.5: - dh = -1 - dh - } - ds := a.s - b.s - dv := a.v - b.v - return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv))) -} - -func toHSV(rgb int) hsv { - r, g, b := float32((rgb&0xFF0000)>>16)/256.0, - float32((rgb&0x00FF00)>>8)/256.0, - float32(rgb&0x0000FF)/256.0 - min, max := minmax3f(r, g, b) - h := max - min - if h > 0 { - if max == r { - h = (g - b) / h - if h < 0 { - h += 6 - } - } else if max == g { - h = 2 + (b-r)/h - } else { - h = 4 + (r-g)/h - } - } - h /= 6.0 - s := max - min - if max != 0 { - s /= max - } - v := max - return hsv{h: h, s: s, v: v} -} - -type hsvTable []hsv - -func toHSVTable(rgbTable []consoleColor) hsvTable { - t := make(hsvTable, len(rgbTable)) - for i, c := range rgbTable { - t[i] = toHSV(c.rgb) - } - return t -} - -func (t hsvTable) find(rgb int) consoleColor { - hsv := toHSV(rgb) - n := 7 - l := float32(5.0) - for i, p := range t { - d := hsv.dist(p) - if d < l { - l, n = d, i - } - } - return color16[n] -} - -func minmax3f(a, b, c float32) (min, max float32) { - if a < b { - if b < c { - return a, c - } else if a < c { - return a, b - } else { - return c, b - } - } else { - if a < c { - return b, c - } else if b < c { - return b, a - } else { - return c, a - } - } -} - -var n256foreAttr []word -var n256backAttr []word - -func n256setup() { - n256foreAttr = make([]word, 256) - n256backAttr = make([]word, 256) - t := toHSVTable(color16) - for i, rgb := range color256 { - c := t.find(rgb) - n256foreAttr[i] = c.foregroundAttr() - n256backAttr[i] = c.backgroundAttr() - } -} diff --git a/vendor/github.com/mattn/go-colorable/noncolorable.go b/vendor/github.com/mattn/go-colorable/noncolorable.go deleted file mode 100644 index 9721e16f..00000000 --- a/vendor/github.com/mattn/go-colorable/noncolorable.go +++ /dev/null @@ -1,55 +0,0 @@ -package colorable - -import ( - "bytes" - "io" -) - -// NonColorable hold writer but remove escape sequence. -type NonColorable struct { - out io.Writer -} - -// NewNonColorable return new instance of Writer which remove escape sequence from Writer. -func NewNonColorable(w io.Writer) io.Writer { - return &NonColorable{out: w} -} - -// Write write data on console -func (w *NonColorable) Write(data []byte) (n int, err error) { - er := bytes.NewReader(data) - var bw [1]byte -loop: - for { - c1, err := er.ReadByte() - if err != nil { - break loop - } - if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) - continue - } - c2, err := er.ReadByte() - if err != nil { - break loop - } - if c2 != 0x5b { - continue - } - - var buf bytes.Buffer - for { - c, err := er.ReadByte() - if err != nil { - break loop - } - if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { - break - } - buf.Write([]byte(string(c))) - } - } - - return len(data), nil -} diff --git a/vendor/github.com/mattn/go-isatty/LICENSE b/vendor/github.com/mattn/go-isatty/LICENSE deleted file mode 100644 index 65dc692b..00000000 --- a/vendor/github.com/mattn/go-isatty/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) Yasuhiro MATSUMOTO - -MIT License (Expat) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/mattn/go-isatty/doc.go b/vendor/github.com/mattn/go-isatty/doc.go deleted file mode 100644 index 17d4f90e..00000000 --- a/vendor/github.com/mattn/go-isatty/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package isatty implements interface to isatty -package isatty diff --git a/vendor/github.com/mattn/go-isatty/isatty_appengine.go b/vendor/github.com/mattn/go-isatty/isatty_appengine.go deleted file mode 100644 index 9584a988..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_appengine.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build appengine - -package isatty - -// IsTerminal returns true if the file descriptor is terminal which -// is always false on on appengine classic which is a sandboxed PaaS. -func IsTerminal(fd uintptr) bool { - return false -} - -// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 -// terminal. This is also always false on this environment. -func IsCygwinTerminal(fd uintptr) bool { - return false -} diff --git a/vendor/github.com/mattn/go-isatty/isatty_bsd.go b/vendor/github.com/mattn/go-isatty/isatty_bsd.go deleted file mode 100644 index 42f2514d..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_bsd.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build darwin freebsd openbsd netbsd dragonfly -// +build !appengine - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TIOCGETA - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/vendor/github.com/mattn/go-isatty/isatty_linux.go b/vendor/github.com/mattn/go-isatty/isatty_linux.go deleted file mode 100644 index 7384cf99..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_linux.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build linux -// +build !appengine,!ppc64,!ppc64le - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TCGETS - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go b/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go deleted file mode 100644 index 44e5d213..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build linux -// +build ppc64 ppc64le - -package isatty - -import ( - "unsafe" - - syscall "golang.org/x/sys/unix" -) - -const ioctlReadTermios = syscall.TCGETS - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/vendor/github.com/mattn/go-isatty/isatty_others.go b/vendor/github.com/mattn/go-isatty/isatty_others.go deleted file mode 100644 index 9d8b4a59..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_others.go +++ /dev/null @@ -1,10 +0,0 @@ -// +build !windows -// +build !appengine - -package isatty - -// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 -// terminal. This is also always false on this environment. -func IsCygwinTerminal(fd uintptr) bool { - return false -} diff --git a/vendor/github.com/mattn/go-isatty/isatty_solaris.go b/vendor/github.com/mattn/go-isatty/isatty_solaris.go deleted file mode 100644 index 1f0c6bf5..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_solaris.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build solaris -// +build !appengine - -package isatty - -import ( - "golang.org/x/sys/unix" -) - -// IsTerminal returns true if the given file descriptor is a terminal. -// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c -func IsTerminal(fd uintptr) bool { - var termio unix.Termio - err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) - return err == nil -} diff --git a/vendor/github.com/mattn/go-isatty/isatty_windows.go b/vendor/github.com/mattn/go-isatty/isatty_windows.go deleted file mode 100644 index af51cbca..00000000 --- a/vendor/github.com/mattn/go-isatty/isatty_windows.go +++ /dev/null @@ -1,94 +0,0 @@ -// +build windows -// +build !appengine - -package isatty - -import ( - "strings" - "syscall" - "unicode/utf16" - "unsafe" -) - -const ( - fileNameInfo uintptr = 2 - fileTypePipe = 3 -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleMode = kernel32.NewProc("GetConsoleMode") - procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx") - procGetFileType = kernel32.NewProc("GetFileType") -) - -func init() { - // Check if GetFileInformationByHandleEx is available. - if procGetFileInformationByHandleEx.Find() != nil { - procGetFileInformationByHandleEx = nil - } -} - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var st uint32 - r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) - return r != 0 && e == 0 -} - -// Check pipe name is used for cygwin/msys2 pty. -// Cygwin/MSYS2 PTY has a name like: -// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master -func isCygwinPipeName(name string) bool { - token := strings.Split(name, "-") - if len(token) < 5 { - return false - } - - if token[0] != `\msys` && token[0] != `\cygwin` { - return false - } - - if token[1] == "" { - return false - } - - if !strings.HasPrefix(token[2], "pty") { - return false - } - - if token[3] != `from` && token[3] != `to` { - return false - } - - if token[4] != "master" { - return false - } - - return true -} - -// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 -// terminal. -func IsCygwinTerminal(fd uintptr) bool { - if procGetFileInformationByHandleEx == nil { - return false - } - - // Cygwin/msys's pty is a pipe. - ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0) - if ft != fileTypePipe || e != 0 { - return false - } - - var buf [2 + syscall.MAX_PATH]uint16 - r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), - 4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)), - uintptr(len(buf)*2), 0, 0) - if r == 0 || e != 0 { - return false - } - - l := *(*uint32)(unsafe.Pointer(&buf)) - return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2]))) -}