diff --git a/BUILD.md b/BUILD.md index c00ee1ef..55374f49 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,30 +1,78 @@ # Setup Development Environment -Clone the repo: +## Clone the repo ```sh git clone --branch v2 https://github.com/aws/aws-node-termination-handler.git +cd aws-node-termination-handler ``` -Install build tools +## Install build tools ```sh make toolchain ``` -Configure image repository location +## Set environment variables ```sh -export KO_DOCKER_REPO=my.image.repo/path +export AWS_REGION= +export AWS_ACCOUNT_ID= +export CLUSTER_NAME= ``` -Build and deploy controller to Kubernetes cluster +## Create Image Repositories + +```sh +aws ecr create-repository \ + --repository-name nthv2/controller \ + --image-scanning-configuration scanOnPush=true \ + --region "${AWS_REGION}" + +aws ecr create-repository \ + --repository-name nthv2/webhook \ + --image-scanning-configuration scanOnPush=true \ + --region "${AWS_REGION}" + +export KO_DOCKER_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/nthv2" + +./scripts/docker-login-ecr.sh +``` + +## Create an EKS Cluster + +```sh +envsubst src/resources/eks-cluster.yaml.tmpl | eksctl create cluster -f - +``` + +### Create the Controller IAM Role + +```sh +aws cloudformation deploy \ + --template-file src/resources/controller-iam-role.yaml \ + --stack-name "nthv2-${CLUSTER_NAME}" \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameter-overrides "ClusterName=${CLUSTER_NAME}" + +eksctl create iamserviceaccount \ + --cluster "${CLUSTER_NAME}" \ + --name nthv2 \ + --namespace nthv2 \ + --role-name "${CLUSTER_NAME}-nthv2" \ + --attach-policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/Nthv2ControllerPolicy-${CLUSTER_NAME}" \ + --role-only \ + --approve + +export NTHV2_IAM_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-nthv2 +``` + +## Build and deploy controller to Kubernetes cluster ```sh make apply ``` -Remove deployed controller from Kubernetes cluster +## Remove deployed controller from Kubernetes cluster ```sh make delete diff --git a/src/Makefile b/src/Makefile index 4cd6808b..120c15f5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -2,6 +2,8 @@ PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) CONTROLLER_GEN = $(PROJECT_DIR)/bin/controller-gen KO = $(PROJECT_DIR)/bin/ko ENVTEST = $(PROJECT_DIR)/bin/setup-envtest +HELM_BASE_OPTS ?= --set serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn=${NTHV2_IAM_ROLE_ARN} +GINKGO_BASE_OPTS ?= --coverpkg $(shell head -n 1 $(PROJECT_DIR)/go.mod | cut -s -d ' ' -f 2)/pkg/... # Image URL to use all building/pushing image targets @@ -45,27 +47,24 @@ toolchain: ## Download additional tools. ##@ Development -.PHONY: manifests -manifests: ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases - .PHONY: generate generate: ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: verify verify: ## Run go fmt and go vet against code. - go fmt ./... - go vet ./... + go fmt $(PROJECT_DIR)/... + go vet $(PROJECT_DIR)/... .PHONY: test -test: manifests generate fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out +test: ## Run tests. + go vet $(PROJECT_DIR)/... + ginkgo run $(GINKGO_BASE_OPTS) $(GINKGO_OPTS) $(PROJECT_DIR)/test/ ##@ Build .PHONY: run -run: manifests generate fmt vet ## Run a controller from your host. +run: ## Run a controller from your host. go run ./main.go ##@ Deployment @@ -73,6 +72,7 @@ run: manifests generate fmt vet ## Run a controller from your host. .PHONY: apply apply: ## Deploy the controller into the current kubernetes cluster. helm upgrade --install dev charts/aws-node-termination-handler-2 --namespace nthv2 --create-namespace \ + $(HELM_BASE_OPTS) \ $(HELM_OPTS) \ --set controller.image=$(shell $(KO) publish -B github.com/aws/aws-node-termination-handler/cmd/controller) \ --set webhook.image=$(shell $(KO) publish -B github.com/aws/aws-node-termination-handler/cmd/webhook) \ diff --git a/src/api/v1alpha1/groupversion_info.go b/src/api/v1alpha1/groupversion_info.go index 727f452b..4dbfc0db 100644 --- a/src/api/v1alpha1/groupversion_info.go +++ b/src/api/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright 2022. +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/api/v1alpha1/terminator_logging.go b/src/api/v1alpha1/terminator_logging.go new file mode 100644 index 00000000..93135a36 --- /dev/null +++ b/src/api/v1alpha1/terminator_logging.go @@ -0,0 +1,41 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "go.uber.org/zap/zapcore" +) + +func (t *TerminatorSpec) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddObject("sqs", t.SQS) + enc.AddObject("drain", t.Drain) + return nil +} + +func (s SQSSpec) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("queueURL", s.QueueURL) + return nil +} + +func (d DrainSpec) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddBool("force", d.Force) + enc.AddInt("gracePeriodSeconds", d.GracePeriodSeconds) + enc.AddBool("ignoreAllDaemonSets", d.IgnoreAllDaemonSets) + enc.AddBool("deleteEmptyDirData", d.DeleteEmptyDirData) + enc.AddInt("timeoutSeconds", d.TimeoutSeconds) + return nil +} diff --git a/src/api/v1alpha1/terminator_types.go b/src/api/v1alpha1/terminator_types.go index 78b75851..f742e646 100644 --- a/src/api/v1alpha1/terminator_types.go +++ b/src/api/v1alpha1/terminator_types.go @@ -1,5 +1,5 @@ /* -Copyright 2022. +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import ( "context" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "knative.dev/pkg/apis" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -31,8 +30,24 @@ type TerminatorSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - // Foo is an example field of Terminator. Edit terminator_types.go to remove/update - Foo string `json:"foo,omitempty"` + SQS SQSSpec `json:"sqs,omitempty"` + Drain DrainSpec `json:"drain,omitempty"` +} + +// SQSSpec defines inputs to SQS "receive messages" requests. +type SQSSpec struct { + // https://pkg.go.dev/github.com/aws/aws-sdk-go@v1.38.55/service/sqs#ReceiveMessageInput + QueueURL string `json:"queueURL,omitempty"` +} + +// DrainSpec defines inputs to the cordon and drain operations. +type DrainSpec struct { + // https://pkg.go.dev/k8s.io/kubectl@v0.21.4/pkg/drain#Helper + Force bool `json:"force,omitempty"` + GracePeriodSeconds int `json:"gracePeriodSeconds,omitempty"` + IgnoreAllDaemonSets bool `json:"ignoreAllDaemonSets,omitempty"` + DeleteEmptyDirData bool `json:"deleteEmptyDirData,omitempty"` + TimeoutSeconds int `json:"timeoutSeconds,omitempty"` } // TerminatorStatus defines the observed state of Terminator @@ -58,12 +73,6 @@ func (t *Terminator) SetDefaults(_ context.Context) { // TODO: actually set defaults } -func (t *Terminator) Validate(_ context.Context) *apis.FieldError { - // Stubbed to satisfy interface requirements. - // TODO: actually validate - return nil -} - // TerminatorList contains a list of Terminator //+kubebuilder:object:root=true type TerminatorList struct { diff --git a/src/api/v1alpha1/terminator_validation.go b/src/api/v1alpha1/terminator_validation.go new file mode 100644 index 00000000..bf2ff401 --- /dev/null +++ b/src/api/v1alpha1/terminator_validation.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "net/url" + + "github.com/aws/aws-sdk-go/service/sqs" + + "k8s.io/apimachinery/pkg/util/sets" + + "knative.dev/pkg/apis" +) + +var ( + // https://github.com/aws/aws-sdk-go/blob/v1.38.55/service/sqs/api.go#L3966-L3994 + knownSQSAttributeNames = sets.NewString(sqs.MessageSystemAttributeName_Values()...) +) + +func (t *Terminator) Validate(_ context.Context) (errs *apis.FieldError) { + return errs.Also( + apis.ValidateObjectMetadata(t).ViaField("metadata"), + t.Spec.validate().ViaField("spec"), + ) +} + +func (t *TerminatorSpec) validate() (errs *apis.FieldError) { + return t.SQS.validate().ViaField("sqs") +} + +func (s *SQSSpec) validate() (errs *apis.FieldError) { + if _, err := url.Parse(s.QueueURL); err != nil { + errs = errs.Also(apis.ErrInvalidValue(s.QueueURL, "queueURL", "must be a valid URL")) + } + return errs +} diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 13e3bae8..a98c656a 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // +build !ignore_autogenerated /* -Copyright 2022. +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,12 +25,42 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DrainSpec) DeepCopyInto(out *DrainSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DrainSpec. +func (in *DrainSpec) DeepCopy() *DrainSpec { + if in == nil { + return nil + } + out := new(DrainSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQSSpec) DeepCopyInto(out *SQSSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQSSpec. +func (in *SQSSpec) DeepCopy() *SQSSpec { + if in == nil { + return nil + } + out := new(SQSSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Terminator) DeepCopyInto(out *Terminator) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -87,6 +117,8 @@ func (in *TerminatorList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TerminatorSpec) DeepCopyInto(out *TerminatorSpec) { *out = *in + in.SQS.DeepCopyInto(&out.SQS) + out.Drain = in.Drain } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TerminatorSpec. diff --git a/src/charts/aws-node-termination-handler-2/crds/node.k8s.aws_terminators.yaml b/src/charts/aws-node-termination-handler-2/crds/node.k8s.aws_terminators.yaml deleted file mode 100644 index 6f609776..00000000 --- a/src/charts/aws-node-termination-handler-2/crds/node.k8s.aws_terminators.yaml +++ /dev/null @@ -1,56 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null - name: terminators.node.k8s.aws -spec: - group: node.k8s.aws - names: - kind: Terminator - listKind: TerminatorList - plural: terminators - singular: terminator - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Terminator is the Schema for the terminators API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TerminatorSpec defines the desired state of Terminator - properties: - foo: - description: Foo is an example field of Terminator. Edit terminator_types.go - to remove/update - type: string - type: object - status: - description: TerminatorStatus defines the observed state of Terminator - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/src/charts/aws-node-termination-handler-2/templates/clusterrole.yaml b/src/charts/aws-node-termination-handler-2/templates/clusterrole.yaml index fb5358c7..d84f4d8e 100644 --- a/src/charts/aws-node-termination-handler-2/templates/clusterrole.yaml +++ b/src/charts/aws-node-termination-handler-2/templates/clusterrole.yaml @@ -20,11 +20,15 @@ rules: - apiGroups: [""] resources: ["nodes"] - verbs: ["get", "list", "patch", "update"] + verbs: ["get", "list", "patch", "update", "watch"] - apiGroups: [""] resources: ["pods"] - verbs: ["get", "list"] + verbs: ["get", "list", "watch"] + + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["pods/eviction"] diff --git a/src/charts/aws-node-termination-handler-2/templates/configmap_logging.yaml b/src/charts/aws-node-termination-handler-2/templates/configmap_logging.yaml index 789b4993..8608e7db 100644 --- a/src/charts/aws-node-termination-handler-2/templates/configmap_logging.yaml +++ b/src/charts/aws-node-termination-handler-2/templates/configmap_logging.yaml @@ -10,31 +10,7 @@ metadata: {{- toYaml . | nindent 8 }} {{- end }} data: - # https://github.com/uber-go/zap/blob/aa3e73ec0896f8b066ddf668597a02f89628ee50/config.go - zap-logger-config: | - { - "level": "{{ .Values.logLevel }}", - "development": false, - "disableStacktrace": true, - "disableCaller": true, - "sampling": { - "initial": 100, - "thereafter": 100 - }, - "outputPaths": ["stdout"], - "errorOutputPaths": ["stderr"], - "encoding": "console", - "encoderConfig": { - "timeKey": "time", - "levelKey": "level", - "nameKey": "logger", - "callerKey": "caller", - "messageKey": "message", - "stacktraceKey": "stacktrace", - "levelEncoder": "capital", - "timeEncoder": "iso8601" - } - } + zap-logger-config: {{ toJson .Values.logging | quote }} {{- with .Values.controller.logLevel }} loglevel.controller: {{ . | quote }} {{- end }} diff --git a/src/charts/aws-node-termination-handler-2/templates/deployment.yaml b/src/charts/aws-node-termination-handler-2/templates/deployment.yaml index 680df31f..5369e434 100644 --- a/src/charts/aws-node-termination-handler-2/templates/deployment.yaml +++ b/src/charts/aws-node-termination-handler-2/templates/deployment.yaml @@ -69,10 +69,26 @@ spec: {{- toYaml . | nindent 22 }} {{- end }} env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace - name: SYSTEM_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace + {{- with .Values.aws.region }} + - name: AWS_REGION + value: {{ . | quote}} + {{- end }} {{- with .Values.controller.env }} {{- toYaml . | nindent 22 }} {{- end }} @@ -111,9 +127,13 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - {{- with .Values.webhook.env }} - {{- toYaml . | nindent 26 }} - {{- end }} + {{- with .Values.aws.region }} + - name: AWS_REGION + value: {{ . | quote}} + {{- end }} + {{- with .Values.webhook.env }} + {{- toYaml . | nindent 26 }} + {{- end }} ports: - name: https-webhook containerPort: {{ .Values.webhook.port }} diff --git a/src/charts/aws-node-termination-handler-2/templates/node.k8s.aws_terminators.yaml b/src/charts/aws-node-termination-handler-2/templates/node.k8s.aws_terminators.yaml new file mode 100644 index 00000000..c0b865e9 --- /dev/null +++ b/src/charts/aws-node-termination-handler-2/templates/node.k8s.aws_terminators.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: terminators.node.k8s.aws +spec: + group: node.k8s.aws + names: + kind: Terminator + listKind: TerminatorList + plural: terminators + singular: terminator + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Terminator is the Schema for the terminators API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TerminatorSpec defines the desired state of Terminator + type: object + properties: + sqs: + description: AWS SQS queue configuration. + type: object + properties: + queueURL: + description: | + The URL of the Amazon SQS queue from which messages are received. + + * Queue URLs and names are case-sensitive. + + * QueueURL is a required field + type: string + drain: + description: TBD + type: object + properties: + force: + description: TBD + type: boolean + {{- with .Values.terminator.defaults.drain.force }} + default: {{ . }} + {{- end }} + gracePeriodSeconds: + description: | + Time to wait for a pod to terminate. + + * A value of 0 will cause pods to be deleted immediately. + + * A negative value will use the pod's terminationGracePeriodSeconds. + type: integer + {{- with .Values.terminator.defaults.drain.gracePeriodSeconds }} + default: {{ . }} + {{- end }} + ignoreAllDaemonSets: + description: Do not terminate pods managed by a daemon set. + type: boolean + {{- with .Values.terminator.defaults.drain.ignoreAllDaemonSets }} + default: {{ . }} + {{- end }} + deleteEmptyDirData: + description: Terminate pods using emptyDir. The local data will be deleted. + type: boolean + {{- with .Values.terminator.defaults.drain.deleteEmptyDirData }} + default: {{ . }} + {{- end }} + timeoutSeconds: + description: | + Time to wait for a "delete" command to complete. + + * A value of 0 means to wait indefinitly. + type: integer + {{- with .Values.terminator.defaults.drain.timeoutSeconds }} + default: {{ . }} + {{- end }} + status: + description: TerminatorStatus defines the observed state of Terminator + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/src/charts/aws-node-termination-handler-2/templates/service.yaml b/src/charts/aws-node-termination-handler-2/templates/service.yaml index 04e31b30..7089aa08 100644 --- a/src/charts/aws-node-termination-handler-2/templates/service.yaml +++ b/src/charts/aws-node-termination-handler-2/templates/service.yaml @@ -22,3 +22,7 @@ spec: port: 8081 protocol: TCP targetPort: http-probes + - name: https-webhook + port: 443 + protocol: TCP + targetPort: https-webhook diff --git a/src/charts/aws-node-termination-handler-2/values.yaml b/src/charts/aws-node-termination-handler-2/values.yaml index 7cf655a8..1eac2a8f 100644 --- a/src/charts/aws-node-termination-handler-2/values.yaml +++ b/src/charts/aws-node-termination-handler-2/values.yaml @@ -10,8 +10,44 @@ labels: {} # Annotations to add to Kubernetes objects created by Helm deployment. annotations: {} -# Global default log message level. -logLevel: info +terminator: + defaults: + drain: + force: true + gracePeriodSeconds: -1 + ignoreAllDaemonSets: true + deleteEmptyDirData: true + timeoutSeconds: 120 + +aws: + # AWS region to use in API calls. + region: "" + +# Global logging configuration. +# See https://github.com/uber-go/zap/blob/2314926ec34c23ee21f3dd4399438469668f8097/config.go#L58-L94 +# for descriptions of each option. +logging: + level: info + development: false + disableStacktrace: true + disableCaller: true + sampling: + initial: 100 + thereafter: 100 + outputPaths: + - stdout + errorOutputPaths: + - stderr + encoding: console + encoderConfig: + timeKey: time + levelKey: level + nameKey: logger + callerKey: caller + messageKey: message + stacktraceKey: stacktrace + levelEncoder: capital + timeEncoder: iso8601 pod: # Number of aws-node-termination-handler controller instance pods. diff --git a/src/cmd/controller/main.go b/src/cmd/controller/main.go index 8de87b18..b6474f4e 100644 --- a/src/cmd/controller/main.go +++ b/src/cmd/controller/main.go @@ -1,5 +1,5 @@ /* -Copyright 2022. +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,36 +17,70 @@ limitations under the License. package main import ( + "context" "flag" + "fmt" + "os" + "time" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - _ "k8s.io/client-go/plugin/pkg/client/auth" - + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "knative.dev/pkg/configmap/informer" knativeinjection "knative.dev/pkg/injection" "knative.dev/pkg/injection/sharedmain" - "knative.dev/pkg/logging" + knativelogging "knative.dev/pkg/logging" "knative.dev/pkg/signals" "knative.dev/pkg/system" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/healthz" nodev1alpha1 "github.com/aws/aws-node-termination-handler/api/v1alpha1" - "github.com/aws/aws-node-termination-handler/controllers" + "github.com/aws/aws-node-termination-handler/pkg/event" + asgterminateeventv1 "github.com/aws/aws-node-termination-handler/pkg/event/asgterminate/v1" + asgterminateeventv2 "github.com/aws/aws-node-termination-handler/pkg/event/asgterminate/v2" + rebalancerecommendationeventv0 "github.com/aws/aws-node-termination-handler/pkg/event/rebalancerecommendation/v0" + scheduledchangeeventv1 "github.com/aws/aws-node-termination-handler/pkg/event/scheduledchange/v1" + spotinterruptioneventv1 "github.com/aws/aws-node-termination-handler/pkg/event/spotinterruption/v1" + statechangeeventv1 "github.com/aws/aws-node-termination-handler/pkg/event/statechange/v1" + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/node" + kubectlcordondrainer "github.com/aws/aws-node-termination-handler/pkg/node/cordondrain/kubectl" + nodename "github.com/aws/aws-node-termination-handler/pkg/node/name" + "github.com/aws/aws-node-termination-handler/pkg/sqsmessage" + "github.com/aws/aws-node-termination-handler/pkg/terminator" + terminatoradapter "github.com/aws/aws-node-termination-handler/pkg/terminator/adapter" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/go-logr/zapr" //+kubebuilder:scaffold:imports ) -const component = "controller" +const componentName = "controller" var scheme = runtime.NewScheme() +type Options struct { + AWSRegion string + MetricsAddress string + EnableLeaderElection bool + ProbeAddress string +} + func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) @@ -55,63 +89,156 @@ func init() { } func main() { - var metricsAddr string - var enableLeaderElection bool - var probeAddr string - flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") - flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, - "Enable leader election for controller manager. "+ - "Enabling this will ensure there is only one active controller manager.") - flag.Parse() - + options := parseOptions() config := ctrl.GetConfigOrDie() config.UserAgent = "aws-node-termination-handler" ctx, startInformers := knativeinjection.EnableInjectionOrDie(signals.NewContext(), config) - rootLogger, atomicLevel := sharedmain.SetupLoggerOrDie(ctx, component) - ctx = logging.WithLogger(ctx, rootLogger) - ctrlLogger := rootLogger.With("component", component) - cmw := informer.NewInformedWatcher(kubernetes.NewForConfigOrDie(config), system.Namespace()) + logger, atomicLevel := sharedmain.SetupLoggerOrDie(ctx, componentName) + ctx = logging.WithLogger(ctx, logger) + clientSet := kubernetes.NewForConfigOrDie(config) + cmw := informer.NewInformedWatcher(clientSet, system.Namespace()) - ctrl.SetLogger(zapr.NewLogger(rootLogger.Desugar())) - rest.SetDefaultWarningHandler(&logging.WarningHandler{Logger: rootLogger}) - sharedmain.WatchLoggingConfigOrDie(ctx, cmw, rootLogger, atomicLevel, component) + ctrl.SetLogger(zapr.NewLogger(logger.Desugar())) + rest.SetDefaultWarningHandler(&knativelogging.WarningHandler{Logger: logger}) + sharedmain.WatchLoggingConfigOrDie(ctx, cmw, logger, atomicLevel, componentName) if err := cmw.Start(ctx.Done()); err != nil { - ctrlLogger.With("error", err).Fatal("failed to watch logging configuration") + logger.With("error", err).Fatal("failed to watch logging configuration") } startInformers() mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, - MetricsBindAddress: metricsAddr, + MetricsBindAddress: options.MetricsAddress, Port: 9443, - HealthProbeBindAddress: probeAddr, - LeaderElection: enableLeaderElection, + HealthProbeBindAddress: options.ProbeAddress, + LeaderElection: options.EnableLeaderElection, LeaderElectionID: "aws-node-termination-handler.k8s.aws", Logger: zapr.NewLogger(logging.FromContext(ctx).Desugar()), }) if err != nil { - ctrlLogger.With("error", err).Fatal("unable to start manager") + logger.With("error", err).Fatal("failed to create manager") + } + if err = indexNodeName(ctx, mgr.GetFieldIndexer()); err != nil { + logger.With("error", err). + With("type", "Pod"). + With("field", "spec.nodeName"). + Fatal("failed to add field index") + } + kubeClient := mgr.GetClient() + + awsSession, err := newAWSSession(options.AWSRegion) + if err != nil { + logger.With("error", err).Fatal("failed to initialize AWS session") + } + sqsClient := sqs.New(awsSession) + if sqsClient == nil { + logger.Fatal("failed to create SQS client") + } + asgClient := autoscaling.New(awsSession) + if asgClient == nil { + logger.Fatal("failed to create ASG client") + } + ec2Client := ec2.New(awsSession) + if ec2Client == nil { + logger.Fatal("failed to create EC2 client") } - if err = (&controllers.TerminatorReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - ctrlLogger.With("error", err).Fatalw("unable to create controller") + eventParser := event.NewAggregatedParser( + asgterminateeventv1.Parser{ASGLifecycleActionCompleter: asgClient}, + asgterminateeventv2.Parser{ASGLifecycleActionCompleter: asgClient}, + rebalancerecommendationeventv0.Parser{}, + scheduledchangeeventv1.Parser{}, + spotinterruptioneventv1.Parser{}, + statechangeeventv1.Parser{}, + ) + + rec := terminator.Reconciler{ + Name: "terminator", + RequeueInterval: time.Duration(10) * time.Second, + NodeGetter: node.Getter{KubeGetter: kubeClient}, + NodeNameGetter: nodename.Getter{EC2InstancesDescriber: ec2Client}, + SQSClientBuilder: terminatoradapter.SQSMessageClientBuilder{ + SQSMessageClient: sqsmessage.Client{SQSClient: sqsClient}, + }, + SQSMessageParser: terminatoradapter.EventParser{Parser: eventParser}, + Getter: terminatoradapter.Getter{KubeGetter: kubeClient}, + CordonDrainerBuilder: terminatoradapter.CordonDrainerBuilder{ + Builder: kubectlcordondrainer.Builder{ + ClientSet: clientSet, + Cordoner: kubectlcordondrainer.DefaultCordoner, + Drainer: kubectlcordondrainer.DefaultDrainer, + }, + }, + } + if err = rec.BuildController( + ctrl.NewControllerManagedBy(mgr). + WithOptions(controller.Options{MaxConcurrentReconciles: 10}), + ); err != nil { + logger.With("error", err). + With("name", rec.Name). + Fatal("failed to create controller") } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - ctrlLogger.With("error", err).Fatal("unable to set up health check") + logger.With("error", err).Fatal("failed to set up health check") } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - ctrlLogger.With("error", err).Fatal("unable to set up ready check") + logger.With("error", err).Fatal("failed to set up ready check") + } + + if err := mgr.Start(ctx); err != nil { + logger.With("error", err).Fatal("failure from manager") + } +} + +func parseOptions() Options { + options := Options{} + + flag.StringVar(&options.AWSRegion, "aws-region", os.Getenv("AWS_REGION"), "The AWS region for API calls.") + flag.StringVar(&options.MetricsAddress, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&options.ProbeAddress, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&options.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.Parse() + + return options +} + +func newAWSSession(awsRegion string) (*session.Session, error) { + config := aws.NewConfig(). + WithRegion(awsRegion). + WithSTSRegionalEndpoint(endpoints.RegionalSTSEndpoint) + sess, err := session.NewSessionWithOptions(session.Options{ + Config: *config, + SharedConfigState: session.SharedConfigEnable, + }) + if err != nil { + return nil, fmt.Errorf("failed to create AWS session: %w", err) + } + + if sess.Config.Region == nil || *sess.Config.Region == "" { + awsRegion, err := ec2metadata.New(sess).Region() + if err != nil { + return nil, fmt.Errorf("failed to get AWS region: %w", err) + } + sess.Config.Region = aws.String(awsRegion) } - ctrlLogger.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - ctrlLogger.With("error", err).Fatal("problem running manager") + _, err = sess.Config.Credentials.Get() + if err != nil { + return nil, fmt.Errorf("failed to get AWS session credentials: %w", err) } + + return sess, nil +} + +func indexNodeName(ctx context.Context, indexer client.FieldIndexer) error { + return indexer.IndexField(ctx, &v1.Pod{}, "spec.nodeName", func(o client.Object) []string { + if o == nil { + return nil + } + return []string{o.(*v1.Pod).Spec.NodeName} + }) } diff --git a/src/cmd/webhook/main.go b/src/cmd/webhook/main.go index b092db9b..e60f087b 100644 --- a/src/cmd/webhook/main.go +++ b/src/cmd/webhook/main.go @@ -1,5 +1,5 @@ /* -Copyright 2022. +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/controllers/suite_test.go b/src/controllers/suite_test.go deleted file mode 100644 index 487cf775..00000000 --- a/src/controllers/suite_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "path/filepath" - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/envtest/printer" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - nodev1alpha1 "github.com/aws/aws-node-termination-handler/api/v1alpha1" - //+kubebuilder:scaffold:imports -) - -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - } - - cfg, err := testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - err = nodev1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - -}, 60) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) -}) diff --git a/src/controllers/terminator_controller.go b/src/controllers/terminator_controller.go deleted file mode 100644 index 724cc2b2..00000000 --- a/src/controllers/terminator_controller.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - nodev1alpha1 "github.com/aws/aws-node-termination-handler/api/v1alpha1" -) - -// TerminatorReconciler reconciles a Terminator object -type TerminatorReconciler struct { - client.Client - Scheme *runtime.Scheme -} - -//+kubebuilder:rbac:groups=node.k8s.aws,resources=terminators,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=node.k8s.aws,resources=terminators/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=node.k8s.aws,resources=terminators/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Terminator object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile -func (r *TerminatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - - // TODO(user): your logic here - - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *TerminatorReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&nodev1alpha1.Terminator{}). - Complete(r) -} diff --git a/src/go.mod b/src/go.mod index 4c924631..3cfa9840 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,10 +3,13 @@ module github.com/aws/aws-node-termination-handler go 1.17 require ( + github.com/go-logr/zapr v0.4.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.18.1 + k8s.io/api v0.21.4 k8s.io/apimachinery v0.21.4 k8s.io/client-go v0.21.4 + k8s.io/kubectl v0.21.4 knative.dev/pkg v0.0.0-20211120133512-d016976f2567 sigs.k8s.io/controller-runtime v0.9.7 ) @@ -15,12 +18,17 @@ require ( cloud.google.com/go v0.98.0 // indirect contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.18 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/aws/aws-sdk-go v1.38.55 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/blendle/zapdriver v1.3.1 // indirect @@ -29,47 +37,72 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/go-errors/errors v1.0.1 // indirect github.com/go-kit/log v0.1.0 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect github.com/go-logr/logr v0.4.0 // indirect - github.com/go-logr/zapr v0.4.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/spec v0.19.5 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/gobuffalo/flect v0.2.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/nxadm/tail v1.4.8 // indirect + github.com/onsi/ginkgo/v2 v2.1.3 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/prometheus/statsd_exporter v0.21.0 // indirect + github.com/russross/blackfriday v1.5.2 // indirect + github.com/spf13/cobra v1.2.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect go.opencensus.io v0.23.0 // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/automaxprocs v1.4.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.19.1 // indirect - golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 // indirect - golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect + golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect + golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect @@ -82,13 +115,15 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/api v0.21.4 // indirect k8s.io/apiextensions-apiserver v0.21.4 // indirect + k8s.io/cli-runtime v0.21.4 // indirect k8s.io/component-base v0.21.4 // indirect k8s.io/klog v1.0.0 // indirect k8s.io/klog/v2 v2.10.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect + sigs.k8s.io/kustomize/api v0.10.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index e88dbf8c..e7c8f978 100644 --- a/src/go.sum +++ b/src/go.sum @@ -53,6 +53,9 @@ contrib.go.opencensus.io/exporter/prometheus v0.4.0/go.mod h1:o7cosnyfuPVK0tB8q0 contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj3gsxcuytSqtH0dxSWW1RE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= @@ -72,26 +75,37 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/jsonschema v0.0.0-20180308105923-f2c93856175a/go.mod h1:qpebaTNSsyUn5rPSJMsfqEtDw71TTggXM6stUDI16HA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.38.55 h1:1Wv5CE1Zy0hJ6MJUQ1ekFiCsNKBK5W69+towYQ1P4Vs= +github.com/aws/aws-sdk-go v1.38.55/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -100,7 +114,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -111,10 +125,10 @@ github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9cop github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -129,27 +143,34 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-gk v0.0.0-20140819190930-201884a44051/go.mod h1:qm+vckxRlDt0aOla0RYJJVeqHZlWfOm2UIxHaqPB46E= github.com/dgryski/go-gk v0.0.0-20200319235926-a69029f61654/go.mod h1:qm+vckxRlDt0aOla0RYJJVeqHZlWfOm2UIxHaqPB46E= github.com/dgryski/go-lttb v0.0.0-20180810165845-318fcdf10a77/go.mod h1:Va5MyIzkU0rAM92tn3hb3Anb7oz7KcnixF49+2wOMe4= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -157,6 +178,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -171,12 +193,16 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= @@ -187,8 +213,13 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -204,29 +235,64 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= -github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/flect v0.2.4 h1:BSYA8+T60cdyq+vynaSUjqSVI9mDEg9ZfQUXKmfjo4I= github.com/gobuffalo/flect v0.2.4/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -271,6 +337,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= github.com/gonum/diff v0.0.0-20181124234638-500114f11e71/go.mod h1:22dM4PLscQl+Nzf64qNBurVJvfyvZELT0iRW2l/NN70= github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= @@ -282,6 +349,8 @@ github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -320,8 +389,11 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -342,7 +414,9 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -382,6 +456,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/tdigest v0.0.0-20180711151920-a7d76c6f093a/go.mod h1:9GkyshztGufsdPQWjH+ifgnIr3xNUL5syI70g2dzU1o= github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= @@ -392,7 +467,11 @@ github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxy github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -415,6 +494,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -425,17 +505,27 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -445,12 +535,18 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -458,6 +554,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -469,6 +567,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -477,8 +576,9 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -486,13 +586,18 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -501,6 +606,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -540,9 +646,14 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -555,21 +666,30 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/quantile v0.0.0-20150917103942-b0c588724d25/go.mod h1:lbP8tGiBjZ5YWIc2fzuRpTaz0b/53vT6PEs3QuAWzuU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -579,16 +699,22 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tsenart/go-tsz v0.0.0-20180814232043-cdeb9e1e981e/go.mod h1:SWZznP1z5Ki7hDT2ioqiFKEse8K9tU2OUvaRI0NeGQo= github.com/tsenart/vegeta/v12 v12.8.4/go.mod h1:ZiJtwLn/9M4fTPdMY7bdbIeyNeFVE8/AHbWFqCsUuho= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -599,6 +725,12 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -608,6 +740,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -622,23 +756,28 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8= golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -680,6 +819,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -687,9 +827,11 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -728,8 +870,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -741,6 +883,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -770,6 +913,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -781,6 +925,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -816,6 +961,7 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -833,13 +979,14 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -861,6 +1008,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -871,6 +1019,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -953,6 +1102,7 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= @@ -1042,6 +1192,7 @@ google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ6 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -1096,6 +1247,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -1107,6 +1259,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -1116,6 +1269,7 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1131,11 +1285,14 @@ k8s.io/apiextensions-apiserver v0.21.4/go.mod h1:OoC8LhI9LnV+wKjZkXIBbLUwtnOGJiT k8s.io/apimachinery v0.21.4 h1:KDq0lWZVslHkuE5I7iGAQHwpK0aDTlar1E7IWEc4CNw= k8s.io/apimachinery v0.21.4/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= k8s.io/apiserver v0.21.4/go.mod h1:SErUuFBBPZUcD2nsUU8hItxoYheqyYr2o/pCINEPW8g= +k8s.io/cli-runtime v0.21.4 h1:kvOzx6dKg+9wRuHTzSqo8tfTV6ixZCkmi+ag54s7mn8= +k8s.io/cli-runtime v0.21.4/go.mod h1:eRbLHYkdVWzvG87yrkgGd8CqX6/+fAG9DTdAqTXmlRY= k8s.io/client-go v0.21.4 h1:tcwj167If+v+pIGrCjaPG7hFo6SqFPFCCgMJy+Vm8Jc= k8s.io/client-go v0.21.4/go.mod h1:t0/eMKyUAq/DoQ7vW8NVVA00/nomlwC+eInsS8PxSew= k8s.io/code-generator v0.21.4/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= k8s.io/component-base v0.21.4 h1:Bc0AttSyhJFVXEIHz+VX+D11j/5z7SPPhl6whiXaRzs= k8s.io/component-base v0.21.4/go.mod h1:ZKG0eHVX+tUDcaoIGpU3Vtk4TIjMddN9uhEWDmW6Nyg= +k8s.io/component-helpers v0.21.4/go.mod h1:/5TBNWmxaAymZweO1JWv3Pt5rcYJV1LbWWY0x1rDdVU= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -1147,11 +1304,13 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.10.0 h1:R2HDMDJsHVTHA2n4RjwbeYXdOcBymXdX/JRb1v0VGhE= k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.40.1 h1:P4RRucWk/lFOlDdkAr3mc7iWFkgKrZY9qZMAgek06S4= -k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kubectl v0.21.4 h1:ODXpSKpi5C6XnJmGg96E/36KAry513v4Jr9Efg3ePJI= +k8s.io/kubectl v0.21.4/go.mod h1:rRYB5HeScoGQKxZDQmus17pTSVIuqfm0D31ApET/qSM= +k8s.io/metrics v0.21.4/go.mod h1:uhWoVuVumUMSeCa1B1p2tm4Y4XuZIg0n24QEtB54wuA= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= @@ -1166,6 +1325,14 @@ rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/controller-runtime v0.9.7 h1:DlHMlAyLpgEITVvNsuZqMbf8/sJl9HirmCZIeR5H9mQ= sigs.k8s.io/controller-runtime v0.9.7/go.mod h1:nExcHcQ2zvLMeoO9K7rOesGCmgu32srN5SENvpAEbGA= +sigs.k8s.io/kustomize/api v0.8.8/go.mod h1:He1zoK0nk43Pc6NlV085xDXDXTNprtcyKZVm3swsdNY= +sigs.k8s.io/kustomize/api v0.10.1 h1:KgU7hfYoscuqag84kxtzKdEC3mKMb99DPI3a0eaV1d0= +sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= +sigs.k8s.io/kustomize/cmd/config v0.9.10/go.mod h1:Mrby0WnRH7hA6OwOYnYpfpiY0WJIMgYrEDfwOeFdMK0= +sigs.k8s.io/kustomize/kustomize/v4 v4.1.2/go.mod h1:PxBvo4WGYlCLeRPL+ziT64wBXqbgfcalOS/SXa/tcyo= +sigs.k8s.io/kustomize/kyaml v0.10.17/go.mod h1:mlQFagmkm1P+W4lZJbJ/yaxMd8PqMRSC4cPcfUVt5Hg= +sigs.k8s.io/kustomize/kyaml v0.13.0 h1:9c+ETyNfSrVhxvphs+K2dzT3dh5oVPPEqPOE/cUpScY= +sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= diff --git a/src/pkg/event/aggregatedparser.go b/src/pkg/event/aggregatedparser.go new file mode 100644 index 00000000..065de052 --- /dev/null +++ b/src/pkg/event/aggregatedparser.go @@ -0,0 +1,63 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package event + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +type ( + Parser interface { + Parse(context.Context, string) terminator.Event + } + + AggregatedParser []Parser +) + +func NewAggregatedParser(parsers ...Parser) AggregatedParser { + return AggregatedParser(parsers) +} + +func (p AggregatedParser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("event.parser")) + + if str == "" { + logging.FromContext(ctx).Warn("nothing to parse") + return noop{} + } + + for _, parser := range p { + if a := parser.Parse(ctx, str); a != nil { + return a + } + } + + logging.FromContext(ctx).Error("failed to parse") + + md := AWSMetadata{} + if err := json.Unmarshal([]byte(str), &md); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal message metadata") + return noop{} + } + return noop(md) +} diff --git a/src/pkg/event/asgterminate/lifecycleaction/complete.go b/src/pkg/event/asgterminate/lifecycleaction/complete.go new file mode 100644 index 00000000..34cdcba5 --- /dev/null +++ b/src/pkg/event/asgterminate/lifecycleaction/complete.go @@ -0,0 +1,55 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lifecycleaction + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/autoscaling" +) + +type ( + ASGLifecycleActionCompleter interface { + CompleteLifecycleActionWithContext(aws.Context, *autoscaling.CompleteLifecycleActionInput, ...request.Option) (*autoscaling.CompleteLifecycleActionOutput, error) + } + + Input struct { + AutoScalingGroupName string + LifecycleActionToken string + LifecycleHookName string + EC2InstanceID string + } +) + +func Complete(ctx context.Context, completer ASGLifecycleActionCompleter, input Input) (bool, error) { + if _, err := completer.CompleteLifecycleActionWithContext(ctx, &autoscaling.CompleteLifecycleActionInput{ + AutoScalingGroupName: aws.String(input.AutoScalingGroupName), + LifecycleActionResult: aws.String("CONTINUE"), + LifecycleHookName: aws.String(input.LifecycleHookName), + LifecycleActionToken: aws.String(input.LifecycleActionToken), + InstanceId: aws.String(input.EC2InstanceID), + }); err != nil { + var f awserr.RequestFailure + return errors.As(err, &f) && f.StatusCode() != 400, err + } + + return false, nil +} diff --git a/src/pkg/event/asgterminate/v1/handler.go b/src/pkg/event/asgterminate/v1/handler.go new file mode 100644 index 00000000..c3c543c9 --- /dev/null +++ b/src/pkg/event/asgterminate/v1/handler.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/pkg/event/asgterminate/lifecycleaction" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type EC2InstanceTerminateLifecycleAction struct { + ASGLifecycleActionCompleter + AWSEvent +} + +func (e EC2InstanceTerminateLifecycleAction) EC2InstanceIDs() []string { + return []string{e.Detail.EC2InstanceID} +} + +func (e EC2InstanceTerminateLifecycleAction) Done(ctx context.Context) (bool, error) { + return lifecycleaction.Complete(ctx, e, lifecycleaction.Input{ + AutoScalingGroupName: e.Detail.AutoScalingGroupName, + LifecycleActionToken: e.Detail.LifecycleActionToken, + LifecycleHookName: e.Detail.LifecycleHookName, + EC2InstanceID: e.Detail.EC2InstanceID, + }) +} + +func (e EC2InstanceTerminateLifecycleAction) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSEvent).AddTo(enc) + return nil +} diff --git a/src/pkg/event/asgterminate/v1/parser.go b/src/pkg/event/asgterminate/v1/parser.go new file mode 100644 index 00000000..e749cefd --- /dev/null +++ b/src/pkg/event/asgterminate/v1/parser.go @@ -0,0 +1,64 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +const ( + source = "aws.autoscaling" + detailType = "EC2 Instance-terminate Lifecycle Action" + version = "1" + acceptedTransition = "autoscaling:EC2_INSTANCE_TERMINATING" +) + +type Parser struct { + ASGLifecycleActionCompleter +} + +func (p Parser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("asgTerminateLifecycleAction.v1")) + + evt := EC2InstanceTerminateLifecycleAction{ + ASGLifecycleActionCompleter: p.ASGLifecycleActionCompleter, + } + if err := json.Unmarshal([]byte(str), &evt); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal EC2 instance-terminate lifecycle action v1 event") + return nil + } + + if evt.Source != source || evt.DetailType != detailType || evt.Version != version { + return nil + } + + if evt.Detail.LifecycleTransition != acceptedTransition { + logging.FromContext(ctx). + With("awsEvent", evt). + With("acceptedTransitions", []string{acceptedTransition}). + Warn("ignorning EC2 instance-terminate lifecycle action event") + return nil + } + + return evt +} diff --git a/src/pkg/event/asgterminate/v1/types.go b/src/pkg/event/asgterminate/v1/types.go new file mode 100644 index 00000000..6c9aae0d --- /dev/null +++ b/src/pkg/event/asgterminate/v1/types.go @@ -0,0 +1,27 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/autoscaling" +) + +type ASGLifecycleActionCompleter interface { + CompleteLifecycleActionWithContext(aws.Context, *autoscaling.CompleteLifecycleActionInput, ...request.Option) (*autoscaling.CompleteLifecycleActionOutput, error) +} diff --git a/src/pkg/event/asgterminate/v1/unmarshal.go b/src/pkg/event/asgterminate/v1/unmarshal.go new file mode 100644 index 00000000..6778802a --- /dev/null +++ b/src/pkg/event/asgterminate/v1/unmarshal.go @@ -0,0 +1,55 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/aws/aws-node-termination-handler/pkg/event" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// AWSEvent contains the properties defined in AWS EventBridge schema +// aws.autoscaling@EC2InstanceTerminateLifecycleAction v1. +type AWSEvent struct { + event.AWSMetadata + + Detail EC2InstanceTerminateLifecycleActionDetail `json:"detail"` +} + +func (e AWSEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSMetadata).AddTo(enc) + enc.AddObject("detail", e.Detail) + return nil +} + +type EC2InstanceTerminateLifecycleActionDetail struct { + LifecycleHookName string `json:"LifecycleHookName"` + LifecycleTransition string `json:"LifecycleTransition"` + AutoScalingGroupName string `json:"AutoScalingGroupName"` + EC2InstanceID string `json:"EC2InstanceId"` + LifecycleActionToken string `json:"LifecycleActionToken"` +} + +func (e EC2InstanceTerminateLifecycleActionDetail) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("LifecycleHookName", e.LifecycleHookName) + enc.AddString("LifecycleTransition", e.LifecycleTransition) + enc.AddString("AutoScalingGroupName", e.AutoScalingGroupName) + enc.AddString("EC2InstanceId", e.EC2InstanceID) + enc.AddString("LifecycleActionToken", e.LifecycleActionToken) + return nil +} diff --git a/src/pkg/event/asgterminate/v2/handler.go b/src/pkg/event/asgterminate/v2/handler.go new file mode 100644 index 00000000..26c727a7 --- /dev/null +++ b/src/pkg/event/asgterminate/v2/handler.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/pkg/event/asgterminate/lifecycleaction" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type EC2InstanceTerminateLifecycleAction struct { + ASGLifecycleActionCompleter + AWSEvent +} + +func (e EC2InstanceTerminateLifecycleAction) EC2InstanceIDs() []string { + return []string{e.Detail.EC2InstanceID} +} + +func (e EC2InstanceTerminateLifecycleAction) Done(ctx context.Context) (bool, error) { + return lifecycleaction.Complete(ctx, e, lifecycleaction.Input{ + AutoScalingGroupName: e.Detail.AutoScalingGroupName, + LifecycleActionToken: e.Detail.LifecycleActionToken, + LifecycleHookName: e.Detail.LifecycleHookName, + EC2InstanceID: e.Detail.EC2InstanceID, + }) +} + +func (e EC2InstanceTerminateLifecycleAction) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSEvent).AddTo(enc) + return nil +} diff --git a/src/pkg/event/asgterminate/v2/parser.go b/src/pkg/event/asgterminate/v2/parser.go new file mode 100644 index 00000000..a810d4da --- /dev/null +++ b/src/pkg/event/asgterminate/v2/parser.go @@ -0,0 +1,64 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +const ( + source = "aws.autoscaling" + detailType = "EC2 Instance-terminate Lifecycle Action" + version = "2" + acceptedTransition = "autoscaling:EC2_INSTANCE_TERMINATING" +) + +type Parser struct { + ASGLifecycleActionCompleter +} + +func (p Parser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("asgTerminateLifecycleAction.v2")) + + evt := EC2InstanceTerminateLifecycleAction{ + ASGLifecycleActionCompleter: p.ASGLifecycleActionCompleter, + } + if err := json.Unmarshal([]byte(str), &evt); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal EC2 instance-terminate lifecycle action v2 event") + return nil + } + + if evt.Source != source || evt.DetailType != detailType || evt.Version != version { + return nil + } + + if evt.Detail.LifecycleTransition != acceptedTransition { + logging.FromContext(ctx). + With("awsEvent", evt). + With("acceptedTransitions", []string{acceptedTransition}). + Warn("ignorning EC2 instance-terminate lifecycle action event") + return nil + } + + return evt +} diff --git a/src/pkg/event/asgterminate/v2/types.go b/src/pkg/event/asgterminate/v2/types.go new file mode 100644 index 00000000..1ce4d963 --- /dev/null +++ b/src/pkg/event/asgterminate/v2/types.go @@ -0,0 +1,27 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/autoscaling" +) + +type ASGLifecycleActionCompleter interface { + CompleteLifecycleActionWithContext(aws.Context, *autoscaling.CompleteLifecycleActionInput, ...request.Option) (*autoscaling.CompleteLifecycleActionOutput, error) +} diff --git a/src/pkg/event/asgterminate/v2/unmarshal.go b/src/pkg/event/asgterminate/v2/unmarshal.go new file mode 100644 index 00000000..4db5392d --- /dev/null +++ b/src/pkg/event/asgterminate/v2/unmarshal.go @@ -0,0 +1,58 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "github.com/aws/aws-node-termination-handler/pkg/event" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// AWSEvent contains the properties defined in AWS EventBridge schema +// aws.autoscaling@EC2InstanceTerminateLifecycleAction v2. +type AWSEvent struct { + event.AWSMetadata + + Detail EC2InstanceTerminateLifecycleActionDetail `json:"detail"` +} + +func (e AWSEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSMetadata).AddTo(enc) + enc.AddObject("detail", e.Detail) + return nil +} + +type EC2InstanceTerminateLifecycleActionDetail struct { + LifecycleHookName string `json:"LifecycleHookName"` + LifecycleTransition string `json:"LifecycleTransition"` + AutoScalingGroupName string `json:"AutoScalingGroupName"` + EC2InstanceID string `json:"EC2InstanceId"` + LifecycleActionToken string `json:"LifecycleActionToken"` + NotificationMetadata string `json:"NotificationMetadata"` + Origin string `json:"Origin"` + Destination string `json:"Destination"` +} + +func (e EC2InstanceTerminateLifecycleActionDetail) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("LifecycleHookName", e.LifecycleHookName) + enc.AddString("LifecycleTransition", e.LifecycleTransition) + enc.AddString("AutoScalingGroupName", e.AutoScalingGroupName) + enc.AddString("EC2InstanceId", e.EC2InstanceID) + enc.AddString("LifecycleActionToken", e.LifecycleActionToken) + return nil +} diff --git a/src/pkg/event/metadata.go b/src/pkg/event/metadata.go new file mode 100644 index 00000000..96ef260f --- /dev/null +++ b/src/pkg/event/metadata.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package event + +import ( + "time" + + "go.uber.org/zap/zapcore" +) + +type AWSMetadata struct { + Account string `json:"account"` + DetailType string `json:"detail-type"` + ID string `json:"id"` + Region string `json:"region"` + Resources []string `json:"resources"` + Source string `json:"source"` + Time time.Time `json:"time"` + Version string `json:"version"` +} + +func (e AWSMetadata) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("source", e.Source) + enc.AddString("detail-type", e.DetailType) + enc.AddString("id", e.ID) + enc.AddTime("time", e.Time) + enc.AddString("region", e.Region) + enc.AddArray("resources", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, resource := range e.Resources { + enc.AppendString(resource) + } + return nil + })) + enc.AddString("version", e.Version) + enc.AddString("account", e.Account) + return nil +} diff --git a/src/pkg/event/noop.go b/src/pkg/event/noop.go new file mode 100644 index 00000000..a4126cb0 --- /dev/null +++ b/src/pkg/event/noop.go @@ -0,0 +1,39 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package event + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type noop AWSMetadata + +func (n noop) EC2InstanceIDs() []string { + return []string{} +} + +func (n noop) Done(_ context.Context) (bool, error) { + return false, nil +} + +func (n noop) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(AWSMetadata(n)).AddTo(enc) + return nil +} diff --git a/src/pkg/event/rebalancerecommendation/v0/handler.go b/src/pkg/event/rebalancerecommendation/v0/handler.go new file mode 100644 index 00000000..8e873f80 --- /dev/null +++ b/src/pkg/event/rebalancerecommendation/v0/handler.go @@ -0,0 +1,39 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v0 + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type EC2InstanceRebalanceRecommendation AWSEvent + +func (e EC2InstanceRebalanceRecommendation) EC2InstanceIDs() []string { + return []string{e.Detail.InstanceID} +} + +func (e EC2InstanceRebalanceRecommendation) Done(_ context.Context) (bool, error) { + return false, nil +} + +func (e EC2InstanceRebalanceRecommendation) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(AWSEvent(e)).AddTo(enc) + return nil +} diff --git a/src/pkg/event/rebalancerecommendation/v0/parser.go b/src/pkg/event/rebalancerecommendation/v0/parser.go new file mode 100644 index 00000000..52511d43 --- /dev/null +++ b/src/pkg/event/rebalancerecommendation/v0/parser.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v0 + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +const ( + source = "aws.ec2" + detailType = "EC2 Instance Rebalance Recommendation" + version = "0" +) + +type Parser struct{} + +func (Parser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("rebalanceRecommendation.v0")) + + evt := EC2InstanceRebalanceRecommendation{} + if err := json.Unmarshal([]byte(str), &evt); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal EC2 instance rebalance recommendation event") + return nil + } + + if evt.Source != source || evt.DetailType != detailType || evt.Version != version { + return nil + } + + return evt +} diff --git a/src/pkg/event/rebalancerecommendation/v0/unmarshal.go b/src/pkg/event/rebalancerecommendation/v0/unmarshal.go new file mode 100644 index 00000000..917cd94b --- /dev/null +++ b/src/pkg/event/rebalancerecommendation/v0/unmarshal.go @@ -0,0 +1,47 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v0 + +import ( + "github.com/aws/aws-node-termination-handler/pkg/event" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// AWSEvent contains the properties defined by +// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/rebalance-recommendations.html#monitor-rebalance-recommendations +type AWSEvent struct { + event.AWSMetadata + + Detail EC2InstanceRebalanceRecommendationDetail `json:"detail"` +} + +func (e AWSEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSMetadata).AddTo(enc) + enc.AddObject("detail", e.Detail) + return nil +} + +type EC2InstanceRebalanceRecommendationDetail struct { + InstanceID string `json:"instance-id"` +} + +func (e EC2InstanceRebalanceRecommendationDetail) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("instance-id", e.InstanceID) + return nil +} diff --git a/src/pkg/event/scheduledchange/v1/handler.go b/src/pkg/event/scheduledchange/v1/handler.go new file mode 100644 index 00000000..e1f3ccf9 --- /dev/null +++ b/src/pkg/event/scheduledchange/v1/handler.go @@ -0,0 +1,43 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type AWSHealthEvent AWSEvent + +func (e AWSHealthEvent) EC2InstanceIDs() []string { + ids := make([]string, len(e.Detail.AffectedEntities)) + for i, entity := range e.Detail.AffectedEntities { + ids[i] = entity.EntityValue + } + return ids +} + +func (e AWSHealthEvent) Done(_ context.Context) (bool, error) { + return false, nil +} + +func (e AWSHealthEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(AWSEvent(e)).AddTo(enc) + return nil +} diff --git a/src/pkg/event/scheduledchange/v1/parser.go b/src/pkg/event/scheduledchange/v1/parser.go new file mode 100644 index 00000000..88d8a388 --- /dev/null +++ b/src/pkg/event/scheduledchange/v1/parser.go @@ -0,0 +1,69 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +const ( + source = "aws.health" + detailType = "AWS Health Event" + version = "1" + acceptedService = "EC2" + acceptedEventTypeCategory = "scheduledChange" +) + +type Parser struct{} + +func (Parser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("scheduledChange.v1")) + + evt := AWSHealthEvent{} + if err := json.Unmarshal([]byte(str), &evt); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal AWS health event") + return nil + } + + if evt.Source != source || evt.DetailType != detailType || evt.Version != version { + return nil + } + + if evt.Detail.Service != acceptedService { + logging.FromContext(ctx). + With("eventDetails", evt). + With("acceptedService", acceptedService). + Warn("ignoring AWS health event") + return nil + } + + if evt.Detail.EventTypeCategory != acceptedEventTypeCategory { + logging.FromContext(ctx). + With("eventDetails", evt). + With("acceptedEventTypeCategory", acceptedEventTypeCategory). + Warn("ignoring AWS health event") + return nil + } + + return evt +} diff --git a/src/pkg/event/scheduledchange/v1/unmarshal.go b/src/pkg/event/scheduledchange/v1/unmarshal.go new file mode 100644 index 00000000..d91b488b --- /dev/null +++ b/src/pkg/event/scheduledchange/v1/unmarshal.go @@ -0,0 +1,91 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/aws/aws-node-termination-handler/pkg/event" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// AWSEvent contains the properties defined in AWS EventBridge schema +// aws.health@AWSHealthEvent v1. +type AWSEvent struct { + event.AWSMetadata + + Detail AWSHealthEventDetail `json:"detail"` +} + +func (e AWSEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSMetadata).AddTo(enc) + enc.AddObject("detail", e.Detail) + return nil +} + +type AWSHealthEventDetail struct { + EventARN string `json:"eventArn"` + EventTypeCode string `json:"eventTypeCode"` + Service string `json:"service"` + EventDescription []EventDescription `json:"eventDescription"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + EventTypeCategory string `json:"eventTypeCategory"` + AffectedEntities []AffectedEntity `json:"affectedEntities"` +} + +func (e AWSHealthEventDetail) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("eventArn", e.EventARN) + enc.AddString("eventTypeCode", e.EventTypeCode) + enc.AddString("eventTypeCategory", e.EventTypeCategory) + enc.AddString("service", e.Service) + enc.AddString("startTime", e.StartTime) + enc.AddString("endTime", e.EndTime) + enc.AddArray("eventDescription", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, desc := range e.EventDescription { + enc.AppendObject(desc) + } + return nil + })) + enc.AddArray("affectedEntities", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, entity := range e.AffectedEntities { + enc.AppendObject(entity) + } + return nil + })) + return nil +} + +type EventDescription struct { + LatestDescription string `json:"latestDescription"` + Language string `json:"language"` +} + +func (e EventDescription) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("latestDescription", e.LatestDescription) + enc.AddString("language", e.Language) + return nil +} + +type AffectedEntity struct { + EntityValue string `json:"entityValue"` +} + +func (e AffectedEntity) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("entityValue", e.EntityValue) + return nil +} diff --git a/src/pkg/event/spotinterruption/v1/handler.go b/src/pkg/event/spotinterruption/v1/handler.go new file mode 100644 index 00000000..42c8b1e2 --- /dev/null +++ b/src/pkg/event/spotinterruption/v1/handler.go @@ -0,0 +1,39 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type EC2SpotInstanceInterruptionWarning AWSEvent + +func (e EC2SpotInstanceInterruptionWarning) EC2InstanceIDs() []string { + return []string{e.Detail.InstanceID} +} + +func (e EC2SpotInstanceInterruptionWarning) Done(_ context.Context) (bool, error) { + return false, nil +} + +func (e EC2SpotInstanceInterruptionWarning) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(AWSEvent(e)).AddTo(enc) + return nil +} diff --git a/src/pkg/event/spotinterruption/v1/parser.go b/src/pkg/event/spotinterruption/v1/parser.go new file mode 100644 index 00000000..b1567770 --- /dev/null +++ b/src/pkg/event/spotinterruption/v1/parser.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +const ( + source = "aws.ec2" + detailType = "EC2 Spot Instance Interruption Warning" + version = "1" +) + +type Parser struct{} + +func (Parser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("spotInterruption.v1")) + + evt := EC2SpotInstanceInterruptionWarning{} + if err := json.Unmarshal([]byte(str), &evt); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal EC2 spot instance interruption event") + return nil + } + + if evt.Source != source || evt.DetailType != detailType || evt.Version != version { + return nil + } + + return evt +} diff --git a/src/pkg/event/spotinterruption/v1/unmarshal.go b/src/pkg/event/spotinterruption/v1/unmarshal.go new file mode 100644 index 00000000..1c40a2a0 --- /dev/null +++ b/src/pkg/event/spotinterruption/v1/unmarshal.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/aws/aws-node-termination-handler/pkg/event" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// AWSEvent contains the properties defined in AWS EventBridge schema +// aws.ec2@EC2SpotInstanceInterruptionWarning v1. +type AWSEvent struct { + event.AWSMetadata + + Detail EC2SpotInstanceInterruptionWarningDetail `json:"detail"` +} + +func (e AWSEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSMetadata).AddTo(enc) + enc.AddObject("detail", e.Detail) + return nil +} + +type EC2SpotInstanceInterruptionWarningDetail struct { + InstanceID string `json:"instance-id"` + InstanceAction string `json:"instance-action"` +} + +func (e EC2SpotInstanceInterruptionWarningDetail) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("instance-id", e.InstanceID) + enc.AddString("instance-action", e.InstanceAction) + return nil +} diff --git a/src/pkg/event/statechange/v1/handler.go b/src/pkg/event/statechange/v1/handler.go new file mode 100644 index 00000000..0527f2a1 --- /dev/null +++ b/src/pkg/event/statechange/v1/handler.go @@ -0,0 +1,39 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type EC2InstanceStateChangeNotification AWSEvent + +func (e EC2InstanceStateChangeNotification) EC2InstanceIDs() []string { + return []string{e.Detail.InstanceID} +} + +func (e EC2InstanceStateChangeNotification) Done(_ context.Context) (bool, error) { + return false, nil +} + +func (e EC2InstanceStateChangeNotification) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(AWSEvent(e)).AddTo(enc) + return nil +} diff --git a/src/pkg/event/statechange/v1/parser.go b/src/pkg/event/statechange/v1/parser.go new file mode 100644 index 00000000..2bac7578 --- /dev/null +++ b/src/pkg/event/statechange/v1/parser.go @@ -0,0 +1,63 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "encoding/json" + "strings" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +const ( + source = "aws.ec2" + detailType = "EC2 Instance State-change Notification" + version = "1" + acceptedStates = "stopping,stopped,shutting-down,terminated" +) + +var acceptedStatesList = strings.Split(acceptedStates, ",") + +type Parser struct{} + +func (Parser) Parse(ctx context.Context, str string) terminator.Event { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("stateChange.v1")) + + evt := EC2InstanceStateChangeNotification{} + if err := json.Unmarshal([]byte(str), &evt); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to unmarshal EC2 state-change event") + return nil + } + + if evt.Source != source || evt.DetailType != detailType || evt.Version != version { + return nil + } + + if !strings.Contains(acceptedStates, strings.ToLower(evt.Detail.State)) { + logging.FromContext(ctx). + With("eventDetails", evt). + With("acceptedStates", acceptedStatesList). + Warn("ignorning EC2 state-change notification") + return nil + } + + return evt +} diff --git a/src/pkg/event/statechange/v1/unmarshal.go b/src/pkg/event/statechange/v1/unmarshal.go new file mode 100644 index 00000000..f7765672 --- /dev/null +++ b/src/pkg/event/statechange/v1/unmarshal.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "github.com/aws/aws-node-termination-handler/pkg/event" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// AWSEvent contains the properties defined in AWS EventBridge schema +// aws.ec2@EC2InstanceStateChangeNotification v1. +type AWSEvent struct { + event.AWSMetadata + + Detail EC2InstanceStateChangeNotificationDetail `json:"detail"` +} + +func (e AWSEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + zap.Inline(e.AWSMetadata).AddTo(enc) + enc.AddObject("detail", e.Detail) + return nil +} + +type EC2InstanceStateChangeNotificationDetail struct { + InstanceID string `json:"instance-id"` + State string `json:"state"` +} + +func (e EC2InstanceStateChangeNotificationDetail) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("instance-id", e.InstanceID) + enc.AddString("state", e.State) + return nil +} diff --git a/src/pkg/logging/alias.go b/src/pkg/logging/alias.go new file mode 100644 index 00000000..1bf44110 --- /dev/null +++ b/src/pkg/logging/alias.go @@ -0,0 +1,34 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logging + +import ( + "github.com/aws/aws-node-termination-handler/pkg/logging/sqs" + + knativelogging "knative.dev/pkg/logging" +) + +var ( + NewDeleteMessageInputMarshaler = sqs.NewDeleteMessageInputMarshaler + NewMessageMarshaler = sqs.NewMessageMarshaler + NewReceiveMessageInputMarshaler = sqs.NewReceiveMessageInputMarshaler + + // Alias these so the codebase doesn't have to import two separate packages + // for logging. + WithLogger = knativelogging.WithLogger + FromContext = knativelogging.FromContext +) diff --git a/src/pkg/logging/sqs/deletemessageinput.go b/src/pkg/logging/sqs/deletemessageinput.go new file mode 100644 index 00000000..5a65a50e --- /dev/null +++ b/src/pkg/logging/sqs/deletemessageinput.go @@ -0,0 +1,45 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqs + +import ( + awssqs "github.com/aws/aws-sdk-go/service/sqs" + + "go.uber.org/zap/zapcore" +) + +type deleteMessageInputMarshaler struct { + *awssqs.DeleteMessageInput +} + +func NewDeleteMessageInputMarshaler(input *awssqs.DeleteMessageInput) zapcore.ObjectMarshaler { + return deleteMessageInputMarshaler{DeleteMessageInput: input} +} + +func (d deleteMessageInputMarshaler) MarshalLogObject(enc zapcore.ObjectEncoder) error { + if d.DeleteMessageInput == nil { + return nil + } + + if d.QueueUrl != nil { + enc.AddString("queueUrl", *d.QueueUrl) + } + if d.ReceiptHandle != nil { + enc.AddString("receiptHandle", *d.ReceiptHandle) + } + return nil +} diff --git a/src/pkg/logging/sqs/message.go b/src/pkg/logging/sqs/message.go new file mode 100644 index 00000000..da95227f --- /dev/null +++ b/src/pkg/logging/sqs/message.go @@ -0,0 +1,55 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqs + +import ( + awssqs "github.com/aws/aws-sdk-go/service/sqs" + + "go.uber.org/zap/zapcore" +) + +type messageMarshaler struct { + *awssqs.Message +} + +func NewMessageMarshaler(msg *awssqs.Message) zapcore.ObjectMarshaler { + return messageMarshaler{Message: msg} +} + +func (s messageMarshaler) MarshalLogObject(enc zapcore.ObjectEncoder) error { + if s.Message == nil { + return nil + } + + if s.MessageId != nil { + enc.AddString("messageId", *s.MessageId) + } + enc.AddObject("attributes", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error { + for key, value := range s.Attributes { + enc.AddString(key, *value) + } + return nil + })) + enc.AddObject("messageAttributes", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error { + for key, value := range s.MessageAttributes { + enc.AddString(key, *value.StringValue) + } + return nil + })) + + return nil +} diff --git a/src/pkg/logging/sqs/receivemessagesinput.go b/src/pkg/logging/sqs/receivemessagesinput.go new file mode 100644 index 00000000..c71ef602 --- /dev/null +++ b/src/pkg/logging/sqs/receivemessagesinput.go @@ -0,0 +1,70 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqs + +import ( + awssqs "github.com/aws/aws-sdk-go/service/sqs" + + "go.uber.org/zap/zapcore" +) + +type receiveMessageInputMarshaler struct { + *awssqs.ReceiveMessageInput +} + +func NewReceiveMessageInputMarshaler(input *awssqs.ReceiveMessageInput) zapcore.ObjectMarshaler { + return receiveMessageInputMarshaler{ReceiveMessageInput: input} +} + +func (r receiveMessageInputMarshaler) MarshalLogObject(enc zapcore.ObjectEncoder) error { + if r.ReceiveMessageInput == nil { + return nil + } + + if r.QueueUrl != nil { + enc.AddString("queueUrl", *r.QueueUrl) + } + if r.ReceiveRequestAttemptId != nil { + enc.AddString("receiveAttemptId", *r.ReceiveRequestAttemptId) + } + if r.VisibilityTimeout != nil { + enc.AddInt64("visibilityTimeout", *r.VisibilityTimeout) + } + if r.WaitTimeSeconds != nil { + enc.AddInt64("waitTimeSeconds", *r.WaitTimeSeconds) + } + if r.MaxNumberOfMessages != nil { + enc.AddInt64("maxNumberOfMessages", *r.MaxNumberOfMessages) + } + enc.AddArray("attributeNames", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, attr := range r.AttributeNames { + if attr != nil { + enc.AppendString(*attr) + } + } + return nil + })) + enc.AddArray("messageAttributeNames", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, attr := range r.MessageAttributeNames { + if attr != nil { + enc.AppendString(*attr) + } + } + return nil + })) + return nil +} diff --git a/src/pkg/logging/writer.go b/src/pkg/logging/writer.go new file mode 100644 index 00000000..74028e4f --- /dev/null +++ b/src/pkg/logging/writer.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logging + +import ( + "fmt" + "strings" + + "go.uber.org/zap" +) + +// Writer adapts a logger to an `io.Writer`. +type Writer struct { + *zap.SugaredLogger +} + +// Write converts `buf` to a string and sends it to the underlying logger. +// If the string beings with "warn" or "error" (case in-sensitive) the message +// will be logged at the corresponding level; otherwise the level will be +// "info". +func (w Writer) Write(buf []byte) (int, error) { + if w.SugaredLogger == nil { + return 0, fmt.Errorf("Writer's backing logger is nil") + } + + msg := string(buf) + m := strings.ToLower(msg) + switch { + case strings.HasPrefix(m, "error"): + w.Error(msg) + case strings.HasPrefix(m, "warn"): + w.Warn(msg) + default: + w.Info(msg) + } + + return len(buf), nil +} diff --git a/src/pkg/node/cordondrain/config.go b/src/pkg/node/cordondrain/config.go new file mode 100644 index 00000000..80f3e1c5 --- /dev/null +++ b/src/pkg/node/cordondrain/config.go @@ -0,0 +1,25 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cordondrain + +type Config struct { + Force bool + GracePeriodSeconds int + IgnoreAllDaemonSets bool + DeleteEmptyDirData bool + TimeoutSeconds int +} diff --git a/src/pkg/node/cordondrain/kubectl/builder.go b/src/pkg/node/cordondrain/kubectl/builder.go new file mode 100644 index 00000000..b938a173 --- /dev/null +++ b/src/pkg/node/cordondrain/kubectl/builder.go @@ -0,0 +1,60 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubectl + +import ( + "context" + "time" + + "github.com/aws/aws-node-termination-handler/pkg/node/cordondrain" + + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/kubectl/pkg/drain" +) + +type ( + Cordoner interface { + Cordon(context.Context, *v1.Node, drain.Helper) error + } + + Drainer interface { + Drain(context.Context, *v1.Node, drain.Helper) error + } + + Builder struct { + Cordoner + Drainer + + ClientSet kubernetes.Interface + } +) + +func (b Builder) Build(config cordondrain.Config) (CordonDrainer, error) { + return CordonDrainer{ + Cordoner: b.Cordoner, + Drainer: b.Drainer, + Helper: drain.Helper{ + Client: b.ClientSet, + Force: config.Force, + GracePeriodSeconds: config.GracePeriodSeconds, + IgnoreAllDaemonSets: config.IgnoreAllDaemonSets, + DeleteEmptyDirData: config.DeleteEmptyDirData, + Timeout: time.Duration(config.TimeoutSeconds) * time.Second, + }, + }, nil +} diff --git a/src/pkg/node/cordondrain/kubectl/cordondrainer.go b/src/pkg/node/cordondrain/kubectl/cordondrainer.go new file mode 100644 index 00000000..9b3fe07a --- /dev/null +++ b/src/pkg/node/cordondrain/kubectl/cordondrainer.go @@ -0,0 +1,38 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubectl + +import ( + "context" + + v1 "k8s.io/api/core/v1" + "k8s.io/kubectl/pkg/drain" +) + +type CordonDrainer struct { + Cordoner + Drainer + drain.Helper +} + +func (c CordonDrainer) Cordon(ctx context.Context, node *v1.Node) error { + return c.Cordoner.Cordon(ctx, node, c.Helper) +} + +func (c CordonDrainer) Drain(ctx context.Context, node *v1.Node) error { + return c.Drainer.Drain(ctx, node, c.Helper) +} diff --git a/src/pkg/node/cordondrain/kubectl/cordonfunc.go b/src/pkg/node/cordondrain/kubectl/cordonfunc.go new file mode 100644 index 00000000..64ecdb6a --- /dev/null +++ b/src/pkg/node/cordondrain/kubectl/cordonfunc.go @@ -0,0 +1,57 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubectl + +import ( + "context" + "fmt" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + + v1 "k8s.io/api/core/v1" + "k8s.io/kubectl/pkg/drain" +) + +var DefaultCordoner = CordonFunc(drain.RunCordonOrUncordon) + +type CordonFunc func(*drain.Helper, *v1.Node, bool) error + +func (c CordonFunc) Cordon(ctx context.Context, node *v1.Node, helper drain.Helper) error { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("cordon")) + + if node == nil { + err := fmt.Errorf("argument 'node' is nil") + logging.FromContext(ctx). + With("error", err). + Error("failed to cordon node") + return err + } + + helper.Ctx = ctx + helper.Out = logging.Writer{SugaredLogger: logging.FromContext(ctx)} + helper.ErrOut = helper.Out + + const updateNodeUnschedulable = true + if err := c(&helper, node, updateNodeUnschedulable); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to cordon node") + return err + } + + return nil +} diff --git a/src/pkg/node/cordondrain/kubectl/drainfunc.go b/src/pkg/node/cordondrain/kubectl/drainfunc.go new file mode 100644 index 00000000..85e885b6 --- /dev/null +++ b/src/pkg/node/cordondrain/kubectl/drainfunc.go @@ -0,0 +1,56 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubectl + +import ( + "context" + "fmt" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + + v1 "k8s.io/api/core/v1" + "k8s.io/kubectl/pkg/drain" +) + +var DefaultDrainer = DrainFunc(drain.RunNodeDrain) + +type DrainFunc func(*drain.Helper, string) error + +func (d DrainFunc) Drain(ctx context.Context, node *v1.Node, helper drain.Helper) error { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("drain")) + + if node == nil { + err := fmt.Errorf("argument 'node' is nil") + logging.FromContext(ctx). + With("error", err). + Error("failed to drain node") + return err + } + + helper.Ctx = ctx + helper.Out = logging.Writer{SugaredLogger: logging.FromContext(ctx)} + helper.ErrOut = helper.Out + + if err := d(&helper, node.Name); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to drain node") + return err + } + + return nil +} diff --git a/src/pkg/node/getter.go b/src/pkg/node/getter.go new file mode 100644 index 00000000..5ef20af5 --- /dev/null +++ b/src/pkg/node/getter.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package node + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ( + KubeGetter interface { + Get(context.Context, client.ObjectKey, client.Object) error + } + + Getter struct { + KubeGetter + } +) + +func (g Getter) GetNode(ctx context.Context, nodeName string) (*v1.Node, error) { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("node")) + + node := &v1.Node{} + if err := g.Get(ctx, types.NamespacedName{Name: nodeName}, node); err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to retrieve node") + return nil, err + } + + return node, nil +} diff --git a/src/pkg/node/name/getter.go b/src/pkg/node/name/getter.go new file mode 100644 index 00000000..1f772265 --- /dev/null +++ b/src/pkg/node/name/getter.go @@ -0,0 +1,79 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package name + +import ( + "context" + "fmt" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/ec2" +) + +type ( + EC2InstancesDescriber interface { + DescribeInstancesWithContext(aws.Context, *ec2.DescribeInstancesInput, ...request.Option) (*ec2.DescribeInstancesOutput, error) + } + + Getter struct { + EC2InstancesDescriber + } +) + +func (g Getter) GetNodeName(ctx context.Context, instanceID string) (string, error) { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("nodeName")) + + result, err := g.DescribeInstancesWithContext(ctx, &ec2.DescribeInstancesInput{ + InstanceIds: []*string{aws.String(instanceID)}, + }) + if err != nil { + logging.FromContext(ctx). + With("error", err). + Error("DescribeInstances API call failed") + return "", err + } + + if len(result.Reservations) == 0 || len(result.Reservations[0].Instances) == 0 { + err = fmt.Errorf("no EC2 reservation for instance ID %s", instanceID) + logging.FromContext(ctx). + With("error", err). + Error("EC2 instance not found") + return "", err + } + + if result.Reservations[0].Instances[0].PrivateDnsName == nil { + err = fmt.Errorf("no PrivateDnsName for instance %s", instanceID) + logging.FromContext(ctx). + With("error", err). + Error("EC2 instance has no PrivateDnsName") + return "", err + } + + nodeName := *result.Reservations[0].Instances[0].PrivateDnsName + if nodeName == "" { + err = fmt.Errorf("empty PrivateDnsName for instance %s", instanceID) + logging.FromContext(ctx). + With("error", err). + Error("EC2 instance's PrivateDnsName is empty") + return "", err + } + + return nodeName, nil +} diff --git a/src/pkg/sqsmessage/client.go b/src/pkg/sqsmessage/client.go new file mode 100644 index 00000000..6a334e60 --- /dev/null +++ b/src/pkg/sqsmessage/client.go @@ -0,0 +1,66 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sqsmessage + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/pkg/logging" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/sqs" +) + +type ( + SQSClient interface { + ReceiveMessageWithContext(aws.Context, *sqs.ReceiveMessageInput, ...request.Option) (*sqs.ReceiveMessageOutput, error) + DeleteMessageWithContext(aws.Context, *sqs.DeleteMessageInput, ...request.Option) (*sqs.DeleteMessageOutput, error) + } + + Client struct { + SQSClient + } +) + +func (c Client) GetSQSMessages(ctx context.Context, params *sqs.ReceiveMessageInput) ([]*sqs.Message, error) { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("sqsClient.getMessages")) + + result, err := c.ReceiveMessageWithContext(ctx, params) + if err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to fetch messages") + return nil, err + } + + return result.Messages, nil +} + +func (c Client) DeleteSQSMessage(ctx context.Context, params *sqs.DeleteMessageInput) error { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named("sqsClient.deleteMessage")) + + _, err := c.DeleteMessageWithContext(ctx, params) + if err != nil { + logging.FromContext(ctx). + With("error", err). + Error("failed to delete message") + return err + } + + return nil +} diff --git a/src/pkg/terminator/adapter/cordondrainbuilder.go b/src/pkg/terminator/adapter/cordondrainbuilder.go new file mode 100644 index 00000000..68da0db7 --- /dev/null +++ b/src/pkg/terminator/adapter/cordondrainbuilder.go @@ -0,0 +1,44 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package adapter + +import ( + "fmt" + + "github.com/aws/aws-node-termination-handler/api/v1alpha1" + "github.com/aws/aws-node-termination-handler/pkg/node/cordondrain" + kubectlcordondrain "github.com/aws/aws-node-termination-handler/pkg/node/cordondrain/kubectl" + "github.com/aws/aws-node-termination-handler/pkg/terminator" +) + +type CordonDrainerBuilder struct { + kubectlcordondrain.Builder +} + +func (b CordonDrainerBuilder) NewCordonDrainer(terminator *v1alpha1.Terminator) (terminator.CordonDrainer, error) { + if terminator == nil { + return nil, fmt.Errorf("argument 'terminator' is nil") + } + + return b.Build(cordondrain.Config{ + Force: terminator.Spec.Drain.Force, + GracePeriodSeconds: terminator.Spec.Drain.GracePeriodSeconds, + IgnoreAllDaemonSets: terminator.Spec.Drain.IgnoreAllDaemonSets, + DeleteEmptyDirData: terminator.Spec.Drain.DeleteEmptyDirData, + TimeoutSeconds: terminator.Spec.Drain.TimeoutSeconds, + }) +} diff --git a/src/pkg/terminator/adapter/getter.go b/src/pkg/terminator/adapter/getter.go new file mode 100644 index 00000000..2d39276f --- /dev/null +++ b/src/pkg/terminator/adapter/getter.go @@ -0,0 +1,55 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package adapter + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/api/v1alpha1" + "github.com/aws/aws-node-termination-handler/pkg/logging" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ( + KubeGetter interface { + Get(context.Context, client.ObjectKey, client.Object) error + } + + Getter struct { + KubeGetter + } +) + +func (g Getter) GetTerminator(ctx context.Context, name types.NamespacedName) (*v1alpha1.Terminator, error) { + terminator := &v1alpha1.Terminator{} + if err := g.Get(ctx, name, terminator); err != nil { + if errors.IsNotFound(err) { + logging.FromContext(ctx).Warn("terminator not found") + return nil, nil + } + + logging.FromContext(ctx). + With("error", err). + Error("failed to retrieve terminator") + return nil, err + } + + return terminator, nil +} diff --git a/src/pkg/terminator/adapter/parser.go b/src/pkg/terminator/adapter/parser.go new file mode 100644 index 00000000..4f553573 --- /dev/null +++ b/src/pkg/terminator/adapter/parser.go @@ -0,0 +1,37 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package adapter + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/pkg/event" + "github.com/aws/aws-node-termination-handler/pkg/terminator" + + "github.com/aws/aws-sdk-go/service/sqs" +) + +type EventParser struct { + event.Parser +} + +func (e EventParser) Parse(ctx context.Context, msg *sqs.Message) terminator.Event { + if msg == nil || msg.Body == nil { + return e.Parser.Parse(ctx, "") + } + return e.Parser.Parse(ctx, *msg.Body) +} diff --git a/src/pkg/terminator/adapter/sqsclient.go b/src/pkg/terminator/adapter/sqsclient.go new file mode 100644 index 00000000..a0abadf9 --- /dev/null +++ b/src/pkg/terminator/adapter/sqsclient.go @@ -0,0 +1,88 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package adapter + +import ( + "context" + + "github.com/aws/aws-node-termination-handler/api/v1alpha1" + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/terminator" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sqs" +) + +type ( + SQSMessageClient interface { + GetSQSMessages(context.Context, *sqs.ReceiveMessageInput) ([]*sqs.Message, error) + DeleteSQSMessage(context.Context, *sqs.DeleteMessageInput) error + } + + sqsMessageClient struct { + sqs.DeleteMessageInput + sqs.ReceiveMessageInput + SQSMessageClient + } + + SQSMessageClientBuilder struct { + SQSMessageClient + } +) + +func (s SQSMessageClientBuilder) NewSQSClient(terminator *v1alpha1.Terminator) (terminator.SQSClient, error) { + receiveMessageInput := sqs.ReceiveMessageInput{ + QueueUrl: aws.String(terminator.Spec.SQS.QueueURL), + MaxNumberOfMessages: aws.Int64(10), + VisibilityTimeout: aws.Int64(20), // Seconds + WaitTimeSeconds: aws.Int64(20), // Seconds, maximum for long polling + AttributeNames: []*string{ + aws.String(sqs.MessageSystemAttributeNameSentTimestamp), + }, + MessageAttributeNames: []*string{ + aws.String(sqs.QueueAttributeNameAll), + }, + } + + deleteMessageInput := sqs.DeleteMessageInput{ + QueueUrl: aws.String(terminator.Spec.SQS.QueueURL), + } + + return sqsMessageClient{ + DeleteMessageInput: deleteMessageInput, + ReceiveMessageInput: receiveMessageInput, + SQSMessageClient: s.SQSMessageClient, + }, nil +} + +func (s sqsMessageClient) GetSQSMessages(ctx context.Context) ([]*sqs.Message, error) { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx). + With("params", logging.NewReceiveMessageInputMarshaler(&s.ReceiveMessageInput)), + ) + + return s.SQSMessageClient.GetSQSMessages(ctx, &s.ReceiveMessageInput) +} + +func (s sqsMessageClient) DeleteSQSMessage(ctx context.Context, msg *sqs.Message) error { + s.DeleteMessageInput.ReceiptHandle = msg.ReceiptHandle + + ctx = logging.WithLogger(ctx, logging.FromContext(ctx). + With("params", logging.NewDeleteMessageInputMarshaler(&s.DeleteMessageInput)), + ) + + return s.SQSMessageClient.DeleteSQSMessage(ctx, &s.DeleteMessageInput) +} diff --git a/src/pkg/terminator/reconciler.go b/src/pkg/terminator/reconciler.go new file mode 100644 index 00000000..18a6671b --- /dev/null +++ b/src/pkg/terminator/reconciler.go @@ -0,0 +1,189 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package terminator + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-node-termination-handler/api/v1alpha1" + "github.com/aws/aws-node-termination-handler/pkg/logging" + + "github.com/aws/aws-sdk-go/service/sqs" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "go.uber.org/multierr" + "go.uber.org/zap/zapcore" + + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type ( + CordonDrainer interface { + Cordon(context.Context, *v1.Node) error + Drain(context.Context, *v1.Node) error + } + + CordonDrainerBuilder interface { + NewCordonDrainer(*v1alpha1.Terminator) (CordonDrainer, error) + } + + Event interface { + zapcore.ObjectMarshaler + + Done(context.Context) (tryAgain bool, err error) + EC2InstanceIDs() []string + } + + Getter interface { + GetTerminator(context.Context, types.NamespacedName) (*v1alpha1.Terminator, error) + } + + NodeGetter interface { + GetNode(context.Context, string) (*v1.Node, error) + } + + NodeNameGetter interface { + GetNodeName(context.Context, string) (string, error) + } + + SQSClient interface { + GetSQSMessages(context.Context) ([]*sqs.Message, error) + DeleteSQSMessage(context.Context, *sqs.Message) error + } + + SQSClientBuilder interface { + NewSQSClient(*v1alpha1.Terminator) (SQSClient, error) + } + + SQSMessageParser interface { + Parse(context.Context, *sqs.Message) Event + } + + Reconciler struct { + NodeGetter + NodeNameGetter + SQSClientBuilder + SQSMessageParser + CordonDrainerBuilder + Getter + + Name string + RequeueInterval time.Duration + } +) + +func (r Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).Named(r.Name).With("terminator", req.Name)) + + terminator, err := r.GetTerminator(ctx, req.NamespacedName) + if err != nil { + return reconcile.Result{}, err + } + if terminator == nil { + return reconcile.Result{}, nil + } + + cordondrainer, err := r.NewCordonDrainer(terminator) + if err != nil { + return reconcile.Result{}, err + } + + sqsClient, err := r.NewSQSClient(terminator) + if err != nil { + return reconcile.Result{}, err + } + + sqsMessages, err := sqsClient.GetSQSMessages(ctx) + if err != nil { + return reconcile.Result{}, err + } + + origCtx := ctx + for _, msg := range sqsMessages { + ctx = logging.WithLogger(origCtx, logging.FromContext(origCtx). + With("sqsMessage", logging.NewMessageMarshaler(msg)), + ) + + evt := r.Parse(ctx, msg) + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).With("event", evt)) + + savedCtx := ctx + for _, ec2InstanceID := range evt.EC2InstanceIDs() { + ctx = logging.WithLogger(savedCtx, logging.FromContext(savedCtx). + With("ec2InstanceID", ec2InstanceID), + ) + + nodeName, e := r.GetNodeName(ctx, ec2InstanceID) + if e != nil { + err = multierr.Append(err, e) + continue + } + + ctx = logging.WithLogger(ctx, logging.FromContext(ctx).With("node", nodeName)) + + node, e := r.GetNode(ctx, nodeName) + if e != nil { + err = multierr.Append(err, e) + continue + } + + if e = cordondrainer.Cordon(ctx, node); e != nil { + err = multierr.Append(err, e) + continue + } + + if e = cordondrainer.Drain(ctx, node); e != nil { + err = multierr.Append(err, e) + continue + } + } + ctx = savedCtx + + tryAgain, e := evt.Done(ctx) + if e != nil { + err = multierr.Append(err, e) + } + + if tryAgain { + continue + } + + err = multierr.Append(err, sqsClient.DeleteSQSMessage(ctx, msg)) + } + ctx = origCtx + + if err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{RequeueAfter: r.RequeueInterval}, nil +} + +func (r Reconciler) BuildController(builder *builder.Builder) error { + if builder == nil { + return fmt.Errorf("argument 'builder' is nil") + } + + return builder. + Named(r.Name). + For(&v1alpha1.Terminator{}). + Complete(r) +} diff --git a/src/resources/controller-iam-role.yaml b/src/resources/controller-iam-role.yaml new file mode 100644 index 00000000..ee89e4f2 --- /dev/null +++ b/src/resources/controller-iam-role.yaml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: Resources used by AWS Node Termination Handler v2 +Parameters: + ClusterName: + Type: String + Description: "EKS cluster name" +Resources: + Nthv2ControllerPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: !Sub "Nthv2ControllerPolicy-${ClusterName}" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Resource: "*" + Action: + - autoscaling:CompleteLifecycleAction + - autoscaling:DescribeAutoScalingInstances + - autoscaling:DescribeTags + - ec2:DescribeInstances + - sqs:DeleteMessage + - sqs:ReceiveMessage diff --git a/src/resources/eks-cluster.yaml.tmpl b/src/resources/eks-cluster.yaml.tmpl new file mode 100644 index 00000000..9f6de787 --- /dev/null +++ b/src/resources/eks-cluster.yaml.tmpl @@ -0,0 +1,17 @@ +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig +metadata: + name: ${CLUSTER_NAME} + region: ${AWS_REGION} + version: "1.21" + tags: + karpenter.sh/discovery: ${CLUSTER_NAME} +managedNodeGroups: + - instanceType: m5.large + amiFamily: AmazonLinux2 + name: ${CLUSTER_NAME}-ng + desiredCapacity: 1 + minSize: 1 + maxSize: 10 +iam: + withOIDC: true diff --git a/src/scripts/docker-login-ecr.sh b/src/scripts/docker-login-ecr.sh new file mode 100755 index 00000000..63093be1 --- /dev/null +++ b/src/scripts/docker-login-ecr.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +aws ecr get-login-password \ + --region "${AWS_REGION:?AWS_REGION is undefined or empty}" | \ + docker login \ + --username AWS \ + --password-stdin \ + "${KO_DOCKER_REPO:?KO_DOCKER_REPO is undefined or empty}" diff --git a/src/test/app_integ_suite_test.go b/src/test/app_integ_suite_test.go new file mode 100644 index 00000000..f215c7d4 --- /dev/null +++ b/src/test/app_integ_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAppIntegration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "App Integration Suite") +} diff --git a/src/test/asgclient.go b/src/test/asgclient.go new file mode 100644 index 00000000..246c2954 --- /dev/null +++ b/src/test/asgclient.go @@ -0,0 +1,33 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/autoscaling" +) + +type ( + CompleteASGLifecycleActionFunc = func(aws.Context, *autoscaling.CompleteLifecycleActionInput, ...request.Option) (*autoscaling.CompleteLifecycleActionOutput, error) + + ASGClient CompleteASGLifecycleActionFunc +) + +func (a ASGClient) CompleteLifecycleActionWithContext(ctx aws.Context, input *autoscaling.CompleteLifecycleActionInput, options ...request.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + return a(ctx, input, options...) +} diff --git a/src/test/ec2client.go b/src/test/ec2client.go new file mode 100644 index 00000000..58c6653c --- /dev/null +++ b/src/test/ec2client.go @@ -0,0 +1,33 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/ec2" +) + +type ( + DescribeEC2InstancesFunc = func(aws.Context, *ec2.DescribeInstancesInput, ...request.Option) (*ec2.DescribeInstancesOutput, error) + + EC2Client DescribeEC2InstancesFunc +) + +func (e EC2Client) DescribeInstancesWithContext(ctx aws.Context, input *ec2.DescribeInstancesInput, options ...request.Option) (*ec2.DescribeInstancesOutput, error) { + return e(ctx, input, options...) +} diff --git a/src/test/kubeclient.go b/src/test/kubeclient.go new file mode 100644 index 00000000..f45418ff --- /dev/null +++ b/src/test/kubeclient.go @@ -0,0 +1,33 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ( + KubeGetFunc = func(context.Context, client.ObjectKey, client.Object) error + + KubeClient KubeGetFunc +) + +func (k KubeClient) Get(ctx context.Context, key client.ObjectKey, object client.Object) error { + return k(ctx, key, object) +} diff --git a/src/test/reconciliation_test.go b/src/test/reconciliation_test.go new file mode 100644 index 00000000..646ee910 --- /dev/null +++ b/src/test/reconciliation_test.go @@ -0,0 +1,1939 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "context" + "errors" + "fmt" + "io" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + kubectl "k8s.io/kubectl/pkg/drain" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/aws/aws-node-termination-handler/api/v1alpha1" + "github.com/aws/aws-node-termination-handler/pkg/event" + asgterminateeventv1 "github.com/aws/aws-node-termination-handler/pkg/event/asgterminate/v1" + asgterminateeventv2 "github.com/aws/aws-node-termination-handler/pkg/event/asgterminate/v2" + rebalancerecommendationeventv0 "github.com/aws/aws-node-termination-handler/pkg/event/rebalancerecommendation/v0" + scheduledchangeeventv1 "github.com/aws/aws-node-termination-handler/pkg/event/scheduledchange/v1" + spotinterruptioneventv1 "github.com/aws/aws-node-termination-handler/pkg/event/spotinterruption/v1" + statechangeeventv1 "github.com/aws/aws-node-termination-handler/pkg/event/statechange/v1" + "github.com/aws/aws-node-termination-handler/pkg/logging" + "github.com/aws/aws-node-termination-handler/pkg/node" + kubectlcordondrain "github.com/aws/aws-node-termination-handler/pkg/node/cordondrain/kubectl" + nodename "github.com/aws/aws-node-termination-handler/pkg/node/name" + "github.com/aws/aws-node-termination-handler/pkg/sqsmessage" + "github.com/aws/aws-node-termination-handler/pkg/terminator" + terminatoradapter "github.com/aws/aws-node-termination-handler/pkg/terminator/adapter" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + awsrequest "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/sqs" +) + +type ( + EC2InstanceID = string + SQSQueueURL = string + NodeName = string + + State string +) + +const ( + StatePending = State("pending") + StateComplete = State("complete") +) + +var _ = Describe("Reconciliation", func() { + const ( + errMsg = "test error" + queueURL = "http://fake-queue.sqs.aws" + ) + + var ( + // Input variables + // These variables are assigned default values during test setup but may be + // modified to represent different cluster states or AWS service responses. + + // Terminators currently in the cluster. + terminators map[types.NamespacedName]*v1alpha1.Terminator + // Nodes currently in the cluster. + nodes map[types.NamespacedName]*v1.Node + // Maps an EC2 instance id to an ASG lifecycle action state value. + asgLifecycleActions map[EC2InstanceID]State + // Maps an EC2 instance id to the corresponding reservation for a node + // in the cluster. + ec2Reservations map[EC2InstanceID]*ec2.Reservation + // Maps a queue URL to a list of messages waiting to be fetched. + sqsQueues map[SQSQueueURL][]*sqs.Message + + // Output variables + // These variables may be modified during reconciliation and should be + // used to verify the resulting cluster state. + + // A lookup table for nodes that were cordoned. + cordonedNodes map[NodeName]bool + // A lookup table for nodes that were drained. + drainedNodes map[NodeName]bool + // Result information returned by the reconciler. + result reconcile.Result + // Error information returned by the reconciler. + err error + + // Other variables + + // Names of all nodes currently in cluster. + nodeNames []NodeName + // Instance IDs for all nodes currently in cluster. + instanceIDs []EC2InstanceID + // Change count of nodes in cluster. + resizeCluster func(nodeCount uint) + // Create an ASG lifecycle action state entry for an EC2 instance ID. + createPendingASGLifecycleAction func(EC2InstanceID) + + // Name of default terminator. + terminatorNamespaceName types.NamespacedName + // Inputs to .Reconcile() + ctx context.Context + request reconcile.Request + + // The reconciler instance under test. + reconciler terminator.Reconciler + + // Stubs + // Default implementations interract with the backing variables listed + // above. A test may put in place alternate behavior when needed. + completeASGLifecycleActionFunc CompleteASGLifecycleActionFunc + describeEC2InstancesFunc DescribeEC2InstancesFunc + kubeGetFunc KubeGetFunc + receiveSQSMessageFunc ReceiveSQSMessageFunc + deleteSQSMessageFunc DeleteSQSMessageFunc + cordonFunc kubectlcordondrain.CordonFunc + drainFunc kubectlcordondrain.DrainFunc + ) + + When("the SQS queue is empty", func() { + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("the SQS queue contains an ASG Lifecycle Notification v1", func() { + When("the lifecycle transition is termination", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "1", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }) + + createPendingASGLifecycleAction(instanceIDs[1]) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("completes the ASG lifecycle action", func() { + Expect(asgLifecycleActions).To(And(HaveKeyWithValue(instanceIDs[1], Equal(StateComplete)), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the lifecycle transition is not termination", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "1", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "test:INVALID" + } + }`, instanceIDs[1])), + }) + + createPendingASGLifecycleAction(instanceIDs[1]) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("does not complete the ASG lifecycle action", func() { + Expect(asgLifecycleActions).To(And(HaveKeyWithValue(instanceIDs[1], Equal(StatePending)), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + }) + + When("the SQS queue contains an ASG Lifecycle Notification v2", func() { + When("the lifecycle transition is termination", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "2", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }) + + createPendingASGLifecycleAction(instanceIDs[1]) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("completes the ASG lifecycle action", func() { + Expect(asgLifecycleActions).To(And(HaveKeyWithValue(instanceIDs[1], Equal(StateComplete)), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the lifecycle transition is not termination", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "2", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "test:INVALID" + } + }`, instanceIDs[1])), + }) + + createPendingASGLifecycleAction(instanceIDs[1]) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("does not complete the ASG lifecycle action", func() { + Expect(asgLifecycleActions).To(And(HaveKeyWithValue(instanceIDs[1], Equal(StatePending)), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + }) + + When("the SQS queue contains a Rebalance Recommendation Notification", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance Rebalance Recommendation", + "version": "0", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the SQS queue contains a Scheduled Change Notification", func() { + When("the service is EC2 and the event type category is scheduled change", func() { + BeforeEach(func() { + resizeCluster(4) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.health", + "detail-type": "AWS Health Event", + "version": "1", + "detail": { + "service": "EC2", + "eventTypeCategory": "scheduledChange", + "affectedEntities": [ + {"entityValue": "%s"}, + {"entityValue": "%s"} + ] + } + }`, instanceIDs[1], instanceIDs[2])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted nodes", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveKey(nodeNames[2]), HaveLen(2))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveKey(nodeNames[2]), HaveLen(2))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the service is not EC2", func() { + BeforeEach(func() { + resizeCluster(4) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.health", + "detail-type": "AWS Health Event", + "version": "1", + "detail": { + "service": "INVALID", + "eventTypeCategory": "scheduledChange", + "affectedEntities": [ + {"entityValue": "%s"}, + {"entityValue": "%s"} + ] + } + }`, instanceIDs[1], instanceIDs[2])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the event type category is not scheduled change", func() { + BeforeEach(func() { + resizeCluster(4) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.health", + "detail-type": "AWS Health Event", + "version": "1", + "detail": { + "service": "EC2", + "eventTypeCategory": "invalid", + "affectedEntities": [ + {"entityValue": "%s"}, + {"entityValue": "%s"} + ] + } + }`, instanceIDs[1], instanceIDs[2])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + }) + + When("the SQS queue contains a Spot Interruption Notification", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the SQS queue contains a State Change (stopping) Notification", func() { + When("the state is stopping", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "stopping" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted nodes", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the state is stopped", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "stopped" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the state is shutting-down", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "shutting-down" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the state is terminated", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "terminated" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the state is not recognized", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "invalid" + } + }`, instanceIDs[1])), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + }) + + When("the SQS queue contains multiple messages", func() { + BeforeEach(func() { + resizeCluster(12) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], + &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "1", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-2"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "2", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[2])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-3"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance Rebalance Recommendation", + "version": "0", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[3])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-4"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.health", + "detail-type": "AWS Health Event", + "version": "1", + "detail": { + "service": "EC2", + "eventTypeCategory": "scheduledChange", + "affectedEntities": [ + {"entityValue": "%s"}, + {"entityValue": "%s"} + ] + } + }`, instanceIDs[4], instanceIDs[5])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-5"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[6])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-6"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "stopping" + } + }`, instanceIDs[7])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-7"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "stopped" + } + }`, instanceIDs[8])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-8"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "shutting-down" + } + }`, instanceIDs[9])), + }, + &sqs.Message{ + ReceiptHandle: aws.String("msg-9"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Instance State-change Notification", + "version": "1", + "detail": { + "instance-id": "%s", + "state": "terminated" + } + }`, instanceIDs[10])), + }, + ) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("cordons and drains only the targeted nodes", func() { + Expect(cordonedNodes).To(And( + HaveKey(nodeNames[1]), + HaveKey(nodeNames[2]), + HaveKey(nodeNames[3]), + HaveKey(nodeNames[4]), + HaveKey(nodeNames[5]), + HaveKey(nodeNames[6]), + HaveKey(nodeNames[7]), + HaveKey(nodeNames[8]), + HaveKey(nodeNames[9]), + HaveKey(nodeNames[10]), + HaveLen(10), + )) + Expect(drainedNodes).To(And( + HaveKey(nodeNames[1]), + HaveKey(nodeNames[2]), + HaveKey(nodeNames[3]), + HaveKey(nodeNames[4]), + HaveKey(nodeNames[5]), + HaveKey(nodeNames[6]), + HaveKey(nodeNames[7]), + HaveKey(nodeNames[8]), + HaveKey(nodeNames[9]), + HaveKey(nodeNames[10]), + HaveLen(10), + )) + }) + + It("deletes the messages from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the SQS queue contains an unrecognized message", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(`{ + "source": "test.suite", + "detail-type": "Not a real notification", + "version": "1", + "detail": {} + }`), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the SQS queue contains a message with no body", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the SQS queue contains an empty message", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(""), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the SQS message cannot be parsed", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(`{ + "source": "test.suite", + "detail-type": "Mal-formed notification", + `), + }) + }) + + It("returns success and requeues the request with the reconciler's configured interval", func() { + Expect(result, err).To(HaveField("RequeueAfter", Equal(reconciler.RequeueInterval))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the terminator cannot be found", func() { + BeforeEach(func() { + delete(terminators, terminatorNamespaceName) + }) + + It("returns success but does not requeue the request", func() { + Expect(result, err).To(BeZero()) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("there is an error getting the terminator", func() { + BeforeEach(func() { + defaultKubeGetFunc := kubeGetFunc + kubeGetFunc = func(ctx context.Context, key client.ObjectKey, object client.Object) error { + switch object.(type) { + case *v1alpha1.Terminator: + return errors.New(errMsg) + default: + return defaultKubeGetFunc(ctx, key, object) + } + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + }) + + When("there is an error getting SQS messages", func() { + BeforeEach(func() { + receiveSQSMessageFunc = func(_ aws.Context, _ *sqs.ReceiveMessageInput, _ ...awsrequest.Option) (*sqs.ReceiveMessageOutput, error) { + return nil, errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + }) + + When("there is an error deleting an SQS message", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + deleteSQSMessageFunc = func(_ context.Context, _ *sqs.DeleteMessageInput, _ ...awsrequest.Option) (*sqs.DeleteMessageOutput, error) { + return nil, errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + }) + + When("there is an error getting the EC2 reservation for the instance ID", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + describeEC2InstancesFunc = func(_ aws.Context, _ *ec2.DescribeInstancesInput, _ ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + return nil, errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("there is no EC2 reservation for the instance ID", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + describeEC2InstancesFunc = func(_ aws.Context, _ *ec2.DescribeInstancesInput, _ ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + return &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{}, + }, nil + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("the EC2 reservation contains no instances", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + describeEC2InstancesFunc = func(_ aws.Context, _ *ec2.DescribeInstancesInput, _ ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + return &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + {Instances: []*ec2.Instance{}}, + }, + }, nil + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("the EC2 reservation's instance has no PrivateDnsName", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + describeEC2InstancesFunc = func(_ aws.Context, _ *ec2.DescribeInstancesInput, _ ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + return &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + {PrivateDnsName: nil}, + }, + }, + }, + }, nil + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("the EC2 reservation's instance's PrivateDnsName empty", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + describeEC2InstancesFunc = func(_ aws.Context, _ *ec2.DescribeInstancesInput, _ ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + return &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + {PrivateDnsName: aws.String("")}, + }, + }, + }, + }, nil + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("there is an error getting the cluster node name for an EC2 instance ID", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + defaultKubeGetFunc := kubeGetFunc + kubeGetFunc = func(ctx context.Context, key client.ObjectKey, object client.Object) error { + switch object.(type) { + case *v1.Node: + return errors.New(errMsg) + default: + return defaultKubeGetFunc(ctx, key, object) + } + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("cordoning a node fails", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + cordonFunc = func(_ *kubectl.Helper, _ *v1.Node, _ bool) error { + return errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("does not cordon or drain any nodes", func() { + Expect(cordonedNodes).To(BeEmpty()) + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("draining a node fails", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + + drainFunc = func(_ *kubectl.Helper, _ string) error { + return errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("cordons the target node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("does not drain the target node", func() { + Expect(drainedNodes).To(BeEmpty()) + }) + }) + + When("completing an ASG Lifecycle Action (v1) fails", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "1", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }) + + completeASGLifecycleActionFunc = func(_ aws.Context, _ *autoscaling.CompleteLifecycleActionInput, _ ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + return nil, errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the request to complete the ASG Lifecycle Action (v1) fails with a status != 400", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "1", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }) + + completeASGLifecycleActionFunc = func(_ aws.Context, _ *autoscaling.CompleteLifecycleActionInput, _ ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + return nil, awserr.NewRequestFailure(awserr.New("", errMsg, errors.New(errMsg)), 404, "") + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("does not delete the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(HaveLen(1)) + }) + }) + + When("completing an ASG Lifecycle Action (v2) fails", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "2", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }) + + completeASGLifecycleActionFunc = func(_ aws.Context, _ *autoscaling.CompleteLifecycleActionInput, _ ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + return nil, errors.New(errMsg) + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("deletes the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(BeEmpty()) + }) + }) + + When("the request to complete the ASG Lifecycle Action (v2) fails with a status != 400", func() { + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "2", + "detail": { + "EC2InstanceId": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, instanceIDs[1])), + }) + + completeASGLifecycleActionFunc = func(_ aws.Context, _ *autoscaling.CompleteLifecycleActionInput, _ ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + return nil, awserr.NewRequestFailure(awserr.New("", errMsg, errors.New(errMsg)), 404, "") + } + }) + + It("does not requeue the request", func() { + Expect(result).To(BeZero()) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(ContainSubstring(errMsg))) + }) + + It("cordons and drains only the targeted node", func() { + Expect(cordonedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + Expect(drainedNodes).To(And(HaveKey(nodeNames[1]), HaveLen(1))) + }) + + It("does not delete the message from the SQS queue", func() { + Expect(sqsQueues[queueURL]).To(HaveLen(1)) + }) + }) + + When("getting messages from a terminator's SQS queue", func() { + const ( + maxNumberOfMessages = int64(10) + visibilityTimeoutSeconds = int64(20) + waitTimeSeconds = int64(20) + ) + var ( + attributeNames = []string{sqs.MessageSystemAttributeNameSentTimestamp} + messageAttributeNames = []string{sqs.QueueAttributeNameAll} + input *sqs.ReceiveMessageInput + ) + + BeforeEach(func() { + terminator, found := terminators[terminatorNamespaceName] + Expect(found).To(BeTrue()) + + terminator.Spec.SQS.QueueURL = queueURL + + defaultReceiveSQSMessageFunc := receiveSQSMessageFunc + receiveSQSMessageFunc = func(ctx aws.Context, in *sqs.ReceiveMessageInput, options ...awsrequest.Option) (*sqs.ReceiveMessageOutput, error) { + input = in + return defaultReceiveSQSMessageFunc(ctx, in, options...) + } + }) + + It("sends the input values from the terminator", func() { + Expect(input).ToNot(BeNil()) + + for i, attrName := range input.AttributeNames { + Expect(attrName).ToNot(BeNil()) + Expect(*attrName).To(Equal(attributeNames[i])) + } + for i, attrName := range input.MessageAttributeNames { + Expect(attrName).ToNot(BeNil()) + Expect(*attrName).To(Equal(messageAttributeNames[i])) + } + + Expect(input.MaxNumberOfMessages).ToNot(BeNil()) + Expect(*input.MaxNumberOfMessages).To(Equal(maxNumberOfMessages)) + + Expect(input.QueueUrl).ToNot(BeNil()) + Expect(*input.QueueUrl).To(Equal(queueURL)) + + Expect(input.VisibilityTimeout).ToNot(BeNil()) + Expect(*input.VisibilityTimeout).To(Equal(visibilityTimeoutSeconds)) + + Expect(input.WaitTimeSeconds).ToNot(BeNil()) + Expect(*input.WaitTimeSeconds).To(Equal(waitTimeSeconds)) + }) + }) + + When("cordoning a node", func() { + const ( + force = true + gracePeriodSeconds = 31 + ignoreAllDaemonSets = true + deleteEmptyDirData = true + ) + var helper *kubectl.Helper + var timeout time.Duration + + BeforeEach(func() { + timeout = 42 * time.Second + + terminator, found := terminators[terminatorNamespaceName] + Expect(found).To(BeTrue()) + + terminator.Spec.Drain.DeleteEmptyDirData = deleteEmptyDirData + terminator.Spec.Drain.Force = force + terminator.Spec.Drain.GracePeriodSeconds = gracePeriodSeconds + terminator.Spec.Drain.IgnoreAllDaemonSets = ignoreAllDaemonSets + terminator.Spec.Drain.TimeoutSeconds = int(timeout.Seconds()) + + defaultCordonFunc := cordonFunc + cordonFunc = func(h *kubectl.Helper, node *v1.Node, desired bool) error { + helper = h + return defaultCordonFunc(h, node, desired) + } + + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + }) + + It("sends the input values from the terminator", func() { + Expect(helper).ToNot(BeNil()) + + Expect(helper).To(And( + HaveField("DeleteEmptyDirData", Equal(deleteEmptyDirData)), + HaveField("Force", Equal(force)), + HaveField("GracePeriodSeconds", Equal(gracePeriodSeconds)), + HaveField("IgnoreAllDaemonSets", Equal(ignoreAllDaemonSets)), + HaveField("Timeout", Equal(timeout)), + )) + }) + + It("sends additional input values", func() { + Expect(helper).ToNot(BeNil()) + + Expect(helper).To(And( + HaveField("Client", Not(BeNil())), + HaveField("Ctx", Not(BeNil())), + HaveField("Out", Not(BeNil())), + HaveField("ErrOut", Not(BeNil())), + )) + }) + }) + + When("draining a node", func() { + const ( + force = true + gracePeriodSeconds = 31 + ignoreAllDaemonSets = true + deleteEmptyDirData = true + ) + var helper *kubectl.Helper + var timeout time.Duration + + BeforeEach(func() { + timeout = 42 * time.Second + + terminator, found := terminators[terminatorNamespaceName] + Expect(found).To(BeTrue()) + + terminator.Spec.Drain.DeleteEmptyDirData = deleteEmptyDirData + terminator.Spec.Drain.Force = force + terminator.Spec.Drain.GracePeriodSeconds = gracePeriodSeconds + terminator.Spec.Drain.IgnoreAllDaemonSets = ignoreAllDaemonSets + terminator.Spec.Drain.TimeoutSeconds = int(timeout.Seconds()) + + defaultDrainFunc := drainFunc + drainFunc = func(h *kubectl.Helper, nodeName string) error { + helper = h + return defaultDrainFunc(h, nodeName) + } + + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.ec2", + "detail-type": "EC2 Spot Instance Interruption Warning", + "version": "1", + "detail": { + "instance-id": "%s" + } + }`, instanceIDs[1])), + }) + }) + + It("sends the input values from the terminator", func() { + Expect(helper).ToNot(BeNil()) + + Expect(helper).To(And( + HaveField("DeleteEmptyDirData", Equal(deleteEmptyDirData)), + HaveField("Force", Equal(force)), + HaveField("GracePeriodSeconds", Equal(gracePeriodSeconds)), + HaveField("IgnoreAllDaemonSets", Equal(ignoreAllDaemonSets)), + HaveField("Timeout", Equal(timeout)), + )) + }) + + It("sends additional values", func() { + Expect(helper).ToNot(BeNil()) + + Expect(helper).To(And( + HaveField("Client", Not(BeNil())), + HaveField("Ctx", Not(BeNil())), + HaveField("Out", Not(BeNil())), + HaveField("ErrOut", Not(BeNil())), + )) + }) + }) + + When("completing an ASG Complete Lifecycle Action", func() { + const ( + autoScalingGroupName = "testAutoScalingGroupName" + lifecycleActionResult = "CONTINUE" + lifecycleHookName = "testLifecycleHookName" + lifecycleActionToken = "testLifecycleActionToken" + ) + var input *autoscaling.CompleteLifecycleActionInput + + BeforeEach(func() { + resizeCluster(3) + + sqsQueues[queueURL] = append(sqsQueues[queueURL], &sqs.Message{ + ReceiptHandle: aws.String("msg-1"), + Body: aws.String(fmt.Sprintf(`{ + "source": "aws.autoscaling", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "version": "1", + "detail": { + "AutoScalingGroupName": "%s", + "EC2InstanceId": "%s", + "LifecycleActionToken": "%s", + "LifecycleHookName": "%s", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } + }`, autoScalingGroupName, instanceIDs[1], lifecycleActionToken, lifecycleHookName)), + }) + + defaultCompleteASGLifecycleActionFunc := completeASGLifecycleActionFunc + completeASGLifecycleActionFunc = func(ctx aws.Context, in *autoscaling.CompleteLifecycleActionInput, options ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + input = in + return defaultCompleteASGLifecycleActionFunc(ctx, in, options...) + } + }) + + It("sends the expected input values", func() { + Expect(input).ToNot(BeNil()) + + Expect(input.AutoScalingGroupName).ToNot(BeNil()) + Expect(*input.AutoScalingGroupName).To(Equal(autoScalingGroupName)) + + Expect(input.LifecycleActionResult).ToNot(BeNil()) + Expect(*input.LifecycleActionResult).To(Equal(lifecycleActionResult)) + + Expect(input.LifecycleHookName).ToNot(BeNil()) + Expect(*input.LifecycleHookName).To(Equal(lifecycleHookName)) + + Expect(input.LifecycleActionToken).ToNot(BeNil()) + Expect(*input.LifecycleActionToken).To(Equal(lifecycleActionToken)) + + Expect(input.InstanceId).ToNot(BeNil()) + Expect(*input.InstanceId).To(Equal(instanceIDs[1])) + }) + }) + + // Setup the starter state: + // * One terminator (terminatorNamedspacedName) + // * The terminator references an empty sqs queue (queueURL) + // * Zero nodes (use resizeCluster()) + // + // Tests should modify the cluster/aws service states as needed. + BeforeEach(func() { + // 1. Initialize variables. + + logger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), + zapcore.AddSync(io.Discard), + zap.NewAtomicLevelAt(zap.DebugLevel), + )) + + ctx = logging.WithLogger(context.Background(), logger.Sugar()) + terminatorNamespaceName = types.NamespacedName{Namespace: "test", Name: "foo"} + request = reconcile.Request{NamespacedName: terminatorNamespaceName} + sqsQueues = map[SQSQueueURL][]*sqs.Message{queueURL: {}} + terminators = map[types.NamespacedName]*v1alpha1.Terminator{ + // For convenience create a terminator that points to the sqs queue. + terminatorNamespaceName: { + Spec: v1alpha1.TerminatorSpec{ + SQS: v1alpha1.SQSSpec{ + QueueURL: queueURL, + }, + }, + }, + } + nodes = map[types.NamespacedName]*v1.Node{} + ec2Reservations = map[EC2InstanceID]*ec2.Reservation{} + cordonedNodes = map[NodeName]bool{} + drainedNodes = map[NodeName]bool{} + + nodeNames = []NodeName{} + instanceIDs = []EC2InstanceID{} + resizeCluster = func(newNodeCount uint) { + for currNodeCount := uint(len(nodes)); currNodeCount < newNodeCount; currNodeCount++ { + nodeName := fmt.Sprintf("node-%d", currNodeCount) + nodeNames = append(nodeNames, nodeName) + nodes[types.NamespacedName{Name: nodeName}] = &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: nodeName}, + } + + instanceID := fmt.Sprintf("instance-%d", currNodeCount) + instanceIDs = append(instanceIDs, instanceID) + ec2Reservations[instanceID] = &ec2.Reservation{ + Instances: []*ec2.Instance{ + {PrivateDnsName: aws.String(nodeName)}, + }, + } + } + + nodeNames = nodeNames[:newNodeCount] + instanceIDs = instanceIDs[:newNodeCount] + } + + asgLifecycleActions = map[EC2InstanceID]State{} + createPendingASGLifecycleAction = func(instanceID EC2InstanceID) { + Expect(asgLifecycleActions).ToNot(HaveKey(instanceID)) + asgLifecycleActions[instanceID] = StatePending + } + + // 2. Setup stub clients. + + describeEC2InstancesFunc = func(ctx aws.Context, input *ec2.DescribeInstancesInput, _ ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + output := ec2.DescribeInstancesOutput{} + for _, instanceID := range input.InstanceIds { + if instanceID == nil { + continue + } + if reservation, found := ec2Reservations[*instanceID]; found { + output.Reservations = append(output.Reservations, reservation) + } + } + return &output, nil + } + + ec2Client := EC2Client(func(ctx aws.Context, input *ec2.DescribeInstancesInput, options ...awsrequest.Option) (*ec2.DescribeInstancesOutput, error) { + return describeEC2InstancesFunc(ctx, input, options...) + }) + + completeASGLifecycleActionFunc = func(ctx aws.Context, input *autoscaling.CompleteLifecycleActionInput, _ ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + Expect(input.InstanceId).ToNot(BeNil()) + if state, found := asgLifecycleActions[*input.InstanceId]; found { + Expect(state).ToNot(Equal(StateComplete)) + asgLifecycleActions[*input.InstanceId] = StateComplete + } + return &autoscaling.CompleteLifecycleActionOutput{}, nil + } + + asgClient := ASGClient(func(ctx aws.Context, input *autoscaling.CompleteLifecycleActionInput, options ...awsrequest.Option) (*autoscaling.CompleteLifecycleActionOutput, error) { + return completeASGLifecycleActionFunc(ctx, input, options...) + }) + + receiveSQSMessageFunc = func(ctx aws.Context, input *sqs.ReceiveMessageInput, options ...awsrequest.Option) (*sqs.ReceiveMessageOutput, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + Expect(input.QueueUrl).ToNot(BeNil()) + + messages, found := sqsQueues[*input.QueueUrl] + Expect(found).To(BeTrue(), "SQS queue does not exist: %q", *input.QueueUrl) + + return &sqs.ReceiveMessageOutput{Messages: append([]*sqs.Message{}, messages...)}, nil + } + + deleteSQSMessageFunc = func(ctx aws.Context, input *sqs.DeleteMessageInput, options ...awsrequest.Option) (*sqs.DeleteMessageOutput, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + Expect(input.QueueUrl).ToNot(BeNil()) + + queue, found := sqsQueues[*input.QueueUrl] + Expect(found).To(BeTrue(), "SQS queue does not exist: %q", *input.QueueUrl) + + updatedQueue := make([]*sqs.Message, 0, len(queue)) + for i, m := range queue { + if m.ReceiptHandle == input.ReceiptHandle { + updatedQueue = append(updatedQueue, queue[:i]...) + updatedQueue = append(updatedQueue, queue[i+1:]...) + break + } + } + sqsQueues[*input.QueueUrl] = updatedQueue + + return &sqs.DeleteMessageOutput{}, nil + } + + sqsClient := SQSClient{ + ReceiveSQSMessageFunc: func(ctx aws.Context, input *sqs.ReceiveMessageInput, options ...awsrequest.Option) (*sqs.ReceiveMessageOutput, error) { + return receiveSQSMessageFunc(ctx, input, options...) + }, + DeleteSQSMessageFunc: func(ctx aws.Context, input *sqs.DeleteMessageInput, options ...awsrequest.Option) (*sqs.DeleteMessageOutput, error) { + return deleteSQSMessageFunc(ctx, input, options...) + }, + } + + kubeGetFunc = func(ctx context.Context, key client.ObjectKey, object client.Object) error { + if err := ctx.Err(); err != nil { + return err + } + + switch out := object.(type) { + case *v1.Node: + n, found := nodes[key] + if !found { + return k8serrors.NewNotFound(schema.GroupResource{}, key.String()) + } + *out = *n + + case *v1alpha1.Terminator: + t, found := terminators[key] + if !found { + return k8serrors.NewNotFound(schema.GroupResource{}, key.String()) + } + *out = *t + + default: + return fmt.Errorf("unknown type: %s", reflect.TypeOf(object).Name()) + } + return nil + } + + kubeClient := KubeClient(func(ctx context.Context, key client.ObjectKey, object client.Object) error { + return kubeGetFunc(ctx, key, object) + }) + + cordonFunc = func(_ *kubectl.Helper, node *v1.Node, desired bool) error { + if _, found := nodes[types.NamespacedName{Name: node.Name}]; !found { + return fmt.Errorf("node does not exist: %q", node.Name) + } + cordonedNodes[node.Name] = true + return nil + } + + drainFunc = func(_ *kubectl.Helper, nodeName string) error { + if _, found := nodes[types.NamespacedName{Name: nodeName}]; !found { + return fmt.Errorf("node does not exist: %q", nodeName) + } + drainedNodes[nodeName] = true + return nil + } + + // 3. Construct the reconciler. + + eventParser := event.NewAggregatedParser( + asgterminateeventv1.Parser{ASGLifecycleActionCompleter: asgClient}, + asgterminateeventv2.Parser{ASGLifecycleActionCompleter: asgClient}, + rebalancerecommendationeventv0.Parser{}, + scheduledchangeeventv1.Parser{}, + spotinterruptioneventv1.Parser{}, + statechangeeventv1.Parser{}, + ) + + cordoner := kubectlcordondrain.CordonFunc(func(h *kubectl.Helper, n *v1.Node, d bool) error { + return cordonFunc(h, n, d) + }) + + drainer := kubectlcordondrain.DrainFunc(func(h *kubectl.Helper, n string) error { + return drainFunc(h, n) + }) + + cordonDrainerBuilder := kubectlcordondrain.Builder{ + ClientSet: &kubernetes.Clientset{}, + Cordoner: cordoner, + Drainer: drainer, + } + + reconciler = terminator.Reconciler{ + Name: "terminator", + RequeueInterval: time.Duration(10) * time.Second, + NodeGetter: node.Getter{KubeGetter: kubeClient}, + NodeNameGetter: nodename.Getter{EC2InstancesDescriber: ec2Client}, + SQSClientBuilder: terminatoradapter.SQSMessageClientBuilder{ + SQSMessageClient: sqsmessage.Client{SQSClient: sqsClient}, + }, + SQSMessageParser: terminatoradapter.EventParser{Parser: eventParser}, + Getter: terminatoradapter.Getter{KubeGetter: kubeClient}, + CordonDrainerBuilder: terminatoradapter.CordonDrainerBuilder{ + Builder: cordonDrainerBuilder, + }, + } + }) + + // Run the reconciliation before each test subject. + JustBeforeEach(func() { + result, err = reconciler.Reconcile(ctx, request) + }) +}) diff --git a/src/test/sqsclient.go b/src/test/sqsclient.go new file mode 100644 index 00000000..ee9c099a --- /dev/null +++ b/src/test/sqsclient.go @@ -0,0 +1,41 @@ +/* +Copyright 2022 Amazon.com, Inc. or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/sqs" +) + +type ( + ReceiveSQSMessageFunc = func(aws.Context, *sqs.ReceiveMessageInput, ...request.Option) (*sqs.ReceiveMessageOutput, error) + DeleteSQSMessageFunc = func(aws.Context, *sqs.DeleteMessageInput, ...request.Option) (*sqs.DeleteMessageOutput, error) + + SQSClient struct { + ReceiveSQSMessageFunc + DeleteSQSMessageFunc + } +) + +func (s SQSClient) ReceiveMessageWithContext(ctx aws.Context, input *sqs.ReceiveMessageInput, options ...request.Option) (*sqs.ReceiveMessageOutput, error) { + return s.ReceiveSQSMessageFunc(ctx, input, options...) +} + +func (s SQSClient) DeleteMessageWithContext(ctx aws.Context, input *sqs.DeleteMessageInput, options ...request.Option) (*sqs.DeleteMessageOutput, error) { + return s.DeleteSQSMessageFunc(ctx, input, options...) +}