From cf967a313fd0be98b7dff3faf4d68a54aa2da70a Mon Sep 17 00:00:00 2001 From: Yang Le Date: Mon, 24 May 2021 18:07:34 +0800 Subject: [PATCH] add e2e test cases Signed-off-by: Yang Le --- Dockerfile | 3 + Makefile | 33 ++ deploy/hub/clusterrole.yaml | 27 ++ deploy/hub/clusterrolebinding.yaml | 12 + deploy/hub/deployment.yaml | 70 +++++ deploy/hub/kustomization.yaml | 44 +++ deploy/hub/managedclusters.crd.yaml | 241 +++++++++++++++ deploy/hub/managedclustersetbindings.crd.yaml | 57 ++++ deploy/hub/managedclustersets.crd.yaml | 131 ++++++++ deploy/hub/namespace.yaml | 4 + deploy/hub/placementdecisions.crd.yaml | 76 +++++ deploy/hub/placements.crd.yaml | 274 +++++++++++++++++ deploy/hub/serviceaccount.yaml | 5 + test/e2e/placement_test.go | 216 +++++++++++++ test/e2e/suite_test.go | 47 +++ vendor/github.com/go-logr/zapr/.gitignore | 3 + vendor/github.com/go-logr/zapr/Gopkg.lock | 52 ++++ vendor/github.com/go-logr/zapr/Gopkg.toml | 38 +++ vendor/github.com/go-logr/zapr/LICENSE | 201 +++++++++++++ vendor/github.com/go-logr/zapr/README.md | 45 +++ vendor/github.com/go-logr/zapr/go.mod | 10 + vendor/github.com/go-logr/zapr/zapr.go | 167 +++++++++++ vendor/modules.txt | 3 + .../controller-runtime/pkg/log/zap/flags.go | 130 ++++++++ .../pkg/log/zap/kube_helpers.go | 129 ++++++++ .../controller-runtime/pkg/log/zap/zap.go | 283 ++++++++++++++++++ 26 files changed, 2301 insertions(+) create mode 100644 deploy/hub/clusterrole.yaml create mode 100644 deploy/hub/clusterrolebinding.yaml create mode 100644 deploy/hub/deployment.yaml create mode 100644 deploy/hub/kustomization.yaml create mode 100644 deploy/hub/managedclusters.crd.yaml create mode 100644 deploy/hub/managedclustersetbindings.crd.yaml create mode 100644 deploy/hub/managedclustersets.crd.yaml create mode 100644 deploy/hub/namespace.yaml create mode 100644 deploy/hub/placementdecisions.crd.yaml create mode 100644 deploy/hub/placements.crd.yaml create mode 100644 deploy/hub/serviceaccount.yaml create mode 100644 test/e2e/placement_test.go create mode 100644 test/e2e/suite_test.go create mode 100644 vendor/github.com/go-logr/zapr/.gitignore create mode 100644 vendor/github.com/go-logr/zapr/Gopkg.lock create mode 100644 vendor/github.com/go-logr/zapr/Gopkg.toml create mode 100644 vendor/github.com/go-logr/zapr/LICENSE create mode 100644 vendor/github.com/go-logr/zapr/README.md create mode 100644 vendor/github.com/go-logr/zapr/go.mod create mode 100644 vendor/github.com/go-logr/zapr/zapr.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go diff --git a/Dockerfile b/Dockerfile index 29dfbe90e..93200e6cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,13 @@ COPY . . ENV GO_PACKAGE github.com/open-cluster-management/placement RUN make build --warn-undefined-variables +RUN make build-e2e --warn-undefined-variables FROM registry.access.redhat.com/ubi8/ubi-minimal:latest ENV USER_UID=10001 + COPY --from=builder /go/src/github.com/open-cluster-management/placement/placement / +COPY --from=builder /go/src/github.com/open-cluster-management/placement/e2e.test / RUN microdnf update && microdnf clean all USER ${USER_UID} diff --git a/Makefile b/Makefile index be9e5f6f0..3b590eb1e 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,14 @@ include $(addprefix ./vendor/github.com/openshift/build-machinery-go/make/, \ # Image URL to use all building/pushing image targets; IMAGE ?= placement +IMAGE_TAG?=latest IMAGE_REGISTRY ?= quay.io/open-cluster-management +IMAGE_NAME?=$(IMAGE_REGISTRY)/$(IMAGE):$(IMAGE_TAG) +KUBECTL?=kubectl +KUSTOMIZE?=$(PERMANENT_TMP_GOPATH)/bin/kustomize +KUSTOMIZE_VERSION?=v3.5.4 +KUSTOMIZE_ARCHIVE_NAME?=kustomize_$(KUSTOMIZE_VERSION)_$(GOHOSTOS)_$(GOHOSTARCH).tar.gz +kustomize_dir:=$(dir $(KUSTOMIZE)) GIT_HOST ?= github.com/open-cluster-management BASE_DIR := $(shell basename $(PWD)) @@ -28,4 +35,30 @@ GO_TEST_PACKAGES :=./pkg/... # It will generate target "image-$(1)" for building the image and binding it as a prerequisite to target "images". $(call build-image,$(IMAGE),$(IMAGE_REGISTRY)/$(IMAGE),./Dockerfile,.) +deploy-hub: ensure-kustomize + cp deploy/hub/kustomization.yaml deploy/hub/kustomization.yaml.tmp + cd deploy/hub && ../../$(KUSTOMIZE) edit set image quay.io/open-cluster-management/placement:latest=$(IMAGE_NAME) + $(KUSTOMIZE) build deploy/hub | $(KUBECTL) apply -f - + mv deploy/hub/kustomization.yaml.tmp deploy/hub/kustomization.yaml + +build-e2e: + go test -c ./test/e2e -mod=vendor + +test-e2e: build-e2e ensure-kustomize deploy-hub + ./e2e.test -test.v -ginkgo.v + +clean-e2e: + $(RM) ./e2e.test + +ensure-kustomize: +ifeq "" "$(wildcard $(KUSTOMIZE))" + $(info Installing kustomize into '$(KUSTOMIZE)') + mkdir -p '$(kustomize_dir)' + curl -s -f -L https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F$(KUSTOMIZE_VERSION)/$(KUSTOMIZE_ARCHIVE_NAME) -o '$(kustomize_dir)$(KUSTOMIZE_ARCHIVE_NAME)' + tar -C '$(kustomize_dir)' -zvxf '$(kustomize_dir)$(KUSTOMIZE_ARCHIVE_NAME)' + chmod +x '$(KUSTOMIZE)'; +else + $(info Using existing kustomize from "$(KUSTOMIZE)") +endif + include ./test/integration-test.mk diff --git a/deploy/hub/clusterrole.yaml b/deploy/hub/clusterrole.yaml new file mode 100644 index 000000000..ecc26bc75 --- /dev/null +++ b/deploy/hub/clusterrole.yaml @@ -0,0 +1,27 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: open-cluster-management:cluster-manager-placement:controller +rules: +# Allow controller to get/list/watch/create/delete configmaps +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch", "create", "delete", "update"] +# Allow controller to create/patch/update events +- apiGroups: ["", "events.k8s.io"] + resources: ["events"] + verbs: ["create", "patch", "update"] +# Allow controller to view managedclusters/managedclustersets/managedclustersetbindings +- apiGroups: ["cluster.open-cluster-management.io"] + resources: ["managedclusters", "managedclustersets", "managedclustersetbindings"] + verbs: ["get", "list", "watch"] +# Allow controller to manage placements/placementdecisions +- apiGroups: ["cluster.open-cluster-management.io"] + resources: ["placements", "placementdecisions"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +- apiGroups: ["cluster.open-cluster-management.io"] + resources: ["placements/status", "placementdecisions/status"] + verbs: ["update", "patch"] +- apiGroups: ["cluster.open-cluster-management.io"] + resources: ["placements/finalizers"] + verbs: ["update"] diff --git a/deploy/hub/clusterrolebinding.yaml b/deploy/hub/clusterrolebinding.yaml new file mode 100644 index 000000000..c0686eb8e --- /dev/null +++ b/deploy/hub/clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: open-cluster-management:cluster-manager-placement:controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: open-cluster-management:cluster-manager-placement:controller +subjects: +- kind: ServiceAccount + namespace: open-cluster-management-hub + name: cluster-manager-placement-controller-sa diff --git a/deploy/hub/deployment.yaml b/deploy/hub/deployment.yaml new file mode 100644 index 000000000..d76952478 --- /dev/null +++ b/deploy/hub/deployment.yaml @@ -0,0 +1,70 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: cluster-manager-placement-controller + namespace: open-cluster-management-hub + labels: + app: clustermanager-controller +spec: + replicas: 1 + selector: + matchLabels: + app: clustermanager-placement-controller + template: + metadata: + labels: + app: clustermanager-placement-controller + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 70 + podAffinityTerm: + topologyKey: failure-domain.beta.kubernetes.io/zone + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - clustermanager-placement-controller + - weight: 30 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - clustermanager-placement-controller + serviceAccountName: cluster-manager-placement-controller-sa + containers: + - name: placement-controller + image: quay.io/open-cluster-management/placement + imagePullPolicy: IfNotPresent + args: + - "/placement" + - "controller" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + runAsNonRoot: true + livenessProbe: + httpGet: + path: /healthz + scheme: HTTPS + port: 8443 + initialDelaySeconds: 2 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + scheme: HTTPS + port: 8443 + initialDelaySeconds: 2 + resources: + requests: + cpu: 100m + memory: 128Mi diff --git a/deploy/hub/kustomization.yaml b/deploy/hub/kustomization.yaml new file mode 100644 index 000000000..c3b643f23 --- /dev/null +++ b/deploy/hub/kustomization.yaml @@ -0,0 +1,44 @@ + +# Adds namespace to all resources. +namespace: open-cluster-management-hub + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +#namePrefix: multicloud- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +# Each entry in this list must resolve to an existing +# resource definition in YAML. These are the resource +# files that kustomize reads, modifies and emits as a +# YAML string, with resources separated by document +# markers ("---"). +# +# General rule here is anything deployed by OLM bundles should go here as well, +# this is used in "make deploy" for developers and should mimic what OLM deploys +# for you. CRDs are an exception to this as we don't want to have to list them all +# here. These are deployed via a "make install" dependency. + +resources: +- ./managedclusters.crd.yaml +- ./managedclustersets.crd.yaml +- ./managedclustersetbindings.crd.yaml +- ./placements.crd.yaml +- ./placementdecisions.crd.yaml +- ./namespace.yaml +- ./serviceaccount.yaml +- ./clusterrolebinding.yaml +- ./clusterrole.yaml +- ./deployment.yaml + +images: +- name: quay.io/open-cluster-management/placement:latest + newName: quay.io/open-cluster-management/placement + newTag: latest +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/deploy/hub/managedclusters.crd.yaml b/deploy/hub/managedclusters.crd.yaml new file mode 100644 index 000000000..b2ce74cf0 --- /dev/null +++ b/deploy/hub/managedclusters.crd.yaml @@ -0,0 +1,241 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: managedclusters.cluster.open-cluster-management.io +spec: + group: cluster.open-cluster-management.io + names: + kind: ManagedCluster + listKind: ManagedClusterList + plural: managedclusters + singular: managedcluster + scope: Cluster + preserveUnknownFields: false + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hubAcceptsClient + name: Hub Accepted + type: boolean + - jsonPath: .spec.managedClusterClientConfigs[*].url + name: Managed Cluster URLs + type: string + - jsonPath: .status.conditions[?(@.type=="ManagedClusterJoined")].status + name: Joined + type: string + - jsonPath: .status.conditions[?(@.type=="ManagedClusterConditionAvailable")].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: "ManagedCluster represents the desired state and current status + of managed cluster. ManagedCluster is a cluster scoped resource. The name + is the cluster UID. \n The cluster join process follows a double opt-in + process: \n 1. Agent on managed cluster creates CSR on hub with cluster + UID and agent name. 2. Agent on managed cluster creates ManagedCluster on + hub. 3. Cluster admin on hub approves the CSR for UID and agent name of + the ManagedCluster. 4. Cluster admin sets spec.acceptClient of ManagedCluster + to true. 5. Cluster admin on managed cluster creates credential of kubeconfig + to hub. \n Once the hub creates the cluster namespace, the Klusterlet agent + on the ManagedCluster pushes the credential to the hub to use against the + kube-apiserver of the ManagedCluster." + type: object + 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: Spec represents a desired configuration for the agent on + the managed cluster. + type: object + properties: + hubAcceptsClient: + description: hubAcceptsClient represents that hub accepts the joining + of Klusterlet agent on the managed cluster with the hub. The default + value is false, and can only be set true when the user on hub has + an RBAC rule to UPDATE on the virtual subresource of managedclusters/accept. + When the value is set true, a namespace whose name is the same as + the name of ManagedCluster is created on the hub. This namespace + represents the managed cluster, also role/rolebinding is created + on the namespace to grant the permision of access from the agent + on the managed cluster. When the value is set to false, the namespace + representing the managed cluster is deleted. + type: boolean + leaseDurationSeconds: + description: LeaseDurationSeconds is used to coordinate the lease + update time of Klusterlet agents on the managed cluster. If its + value is zero, the Klusterlet agent will update its lease every + 60 seconds by default + type: integer + format: int32 + managedClusterClientConfigs: + description: ManagedClusterClientConfigs represents a list of the + apiserver address of the managed cluster. If it is empty, the managed + cluster has no accessible address for the hub to connect with it. + type: array + items: + description: ClientConfig represents the apiserver address of the + managed cluster. TODO include credential to connect to managed + cluster kube-apiserver + type: object + properties: + caBundle: + description: CABundle is the ca bundle to connect to apiserver + of the managed cluster. System certs are used if it is not + set. + type: string + format: byte + url: + description: URL is the URL of apiserver endpoint of the managed + cluster. + type: string + status: + description: Status represents the current status of joined managed cluster + type: object + properties: + allocatable: + description: Allocatable represents the total allocatable resources + on the managed cluster. + type: object + additionalProperties: + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + capacity: + description: Capacity represents the total resource capacity from + all nodeStatuses on the managed cluster. + type: object + additionalProperties: + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + clusterClaims: + description: ClusterClaims represents cluster information that a managed + cluster claims, for example a unique cluster identifier (id.k8s.io) + and kubernetes version (kubeversion.open-cluster-management.io). + They are written from the managed cluster. The set of claims is + not uniform across a fleet, some claims can be vendor or version + specific and may not be included from all managed clusters. + type: array + items: + description: ManagedClusterClaim represents a ClusterClaim collected + from a managed cluster. + type: object + properties: + name: + description: Name is the name of a ClusterClaim resource on + managed cluster. It's a well known or customized name to identify + the claim. + type: string + maxLength: 253 + minLength: 1 + value: + description: Value is a claim-dependent string + type: string + maxLength: 1024 + minLength: 1 + conditions: + description: Conditions contains the different condition statuses + for this managed cluster. + type: array + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: + \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // +listMapKey=type + \ Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + version: + description: Version represents the kubernetes version of the managed + cluster. + type: object + properties: + kubernetes: + description: Kubernetes is the kubernetes version of managed cluster. + type: string + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/hub/managedclustersetbindings.crd.yaml b/deploy/hub/managedclustersetbindings.crd.yaml new file mode 100644 index 000000000..8a532f4e0 --- /dev/null +++ b/deploy/hub/managedclustersetbindings.crd.yaml @@ -0,0 +1,57 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: managedclustersetbindings.cluster.open-cluster-management.io +spec: + group: cluster.open-cluster-management.io + names: + kind: ManagedClusterSetBinding + listKind: ManagedClusterSetBindingList + plural: managedclustersetbindings + singular: managedclustersetbinding + scope: Namespaced + preserveUnknownFields: false + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ManagedClusterSetBinding projects a ManagedClusterSet into a + certain namespace. User is able to create a ManagedClusterSetBinding in + a namespace and bind it to a ManagedClusterSet if they have an RBAC rule + to CREATE on the virtual subresource of managedclustersets/bind. Workloads + created in the same namespace can only be distributed to ManagedClusters + in ManagedClusterSets bound in this namespace by higher level controllers. + type: object + 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: Spec defines the attributes of ManagedClusterSetBinding. + type: object + properties: + clusterSet: + description: ClusterSet is the name of the ManagedClusterSet to bind. + It must match the instance name of the ManagedClusterSetBinding + and cannot change once created. User is allowed to set this field + if they have an RBAC rule to CREATE on the virtual subresource of + managedclustersets/bind. + type: string + minLength: 1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/hub/managedclustersets.crd.yaml b/deploy/hub/managedclustersets.crd.yaml new file mode 100644 index 000000000..e38178678 --- /dev/null +++ b/deploy/hub/managedclustersets.crd.yaml @@ -0,0 +1,131 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: managedclustersets.cluster.open-cluster-management.io +spec: + group: cluster.open-cluster-management.io + names: + kind: ManagedClusterSet + listKind: ManagedClusterSetList + plural: managedclustersets + singular: managedclusterset + scope: Cluster + preserveUnknownFields: false + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ManagedClusterSet defines a group of ManagedClusters that user's + workload can run on. A workload can be defined to deployed on a ManagedClusterSet, + which mean: 1. The workload can run on any ManagedCluster in the ManagedClusterSet + \ 2. The workload cannot run on any ManagedCluster outside the ManagedClusterSet + \ 3. The service exposed by the workload can be shared in any ManagedCluster + in the ManagedClusterSet \n In order to assign a ManagedCluster to a certian + ManagedClusterSet, add a label with name `cluster.open-cluster-management.io/clusterset` + on the ManagedCluster to refers to the ManagedClusterSet. User is not allow + to add/remove this label on a ManagedCluster unless they have a RBAC rule + to CREATE on a virtual subresource of managedclustersets/join. In order + to update this label, user must have the permission on both the old and + new ManagedClusterSet." + type: object + 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: Spec defines the attributes of the ManagedClusterSet + type: object + status: + description: Status represents the current status of the ManagedClusterSet + type: object + properties: + conditions: + description: Conditions contains the different condition statuses + for this ManagedClusterSet. + type: array + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: + \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // +listMapKey=type + \ Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/hub/namespace.yaml b/deploy/hub/namespace.yaml new file mode 100644 index 000000000..d52206eab --- /dev/null +++ b/deploy/hub/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: open-cluster-management-hub diff --git a/deploy/hub/placementdecisions.crd.yaml b/deploy/hub/placementdecisions.crd.yaml new file mode 100644 index 000000000..fc75289ea --- /dev/null +++ b/deploy/hub/placementdecisions.crd.yaml @@ -0,0 +1,76 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: placementdecisions.cluster.open-cluster-management.io +spec: + group: cluster.open-cluster-management.io + names: + kind: PlacementDecision + listKind: PlacementDecisionList + plural: placementdecisions + singular: placementdecision + scope: Namespaced + preserveUnknownFields: false + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "PlacementDecision indicates a decision from a placement PlacementDecision + should has a label cluster.open-cluster-management.io/placement={placement + name} to reference a certain placement. \n If a placement has spec.numberOfClusters + specified, the total number of decisions contained in status.decisions of + PlacementDecisions should always be NumberOfClusters; otherwise, the total + number of decisions should be the number of ManagedClusters which match + the placement requirements. \n Some of the decisions might be empty when + there are no enough ManagedClusters meet the placement requirements." + type: object + 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 + status: + description: Status represents the current status of the PlacementDecision + type: object + required: + - decisions + properties: + decisions: + description: Decisions is a slice of decisions according to a placement + The number of decisions should not be larger than 100 + type: array + items: + description: ClusterDecision represents a decision from a placement + An empty ClusterDecision indicates it is not scheduled yet. + type: object + required: + - clusterName + - reason + properties: + clusterName: + description: ClusterName is the name of the ManagedCluster. + If it is not empty, its value should be unique cross all placement + decisions for the Placement. + type: string + reason: + description: Reason represents the reason why the ManagedCluster + is selected. + type: string + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/hub/placements.crd.yaml b/deploy/hub/placements.crd.yaml new file mode 100644 index 000000000..619a15a89 --- /dev/null +++ b/deploy/hub/placements.crd.yaml @@ -0,0 +1,274 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: placements.cluster.open-cluster-management.io +spec: + group: cluster.open-cluster-management.io + names: + kind: Placement + listKind: PlacementList + plural: placements + singular: placement + scope: Namespaced + preserveUnknownFields: false + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "Placement defines a rule to select a set of ManagedClusters + from the ManagedClusterSets bound to the placement namespace. \n Here is + how the placement policy combines with other selection methods to determine + a matching list of ManagedClusters: 1) Kubernetes clusters are registered + with hub as cluster-scoped ManagedClusters; 2) ManagedClusters are organized + into cluster-scoped ManagedClusterSets; 3) ManagedClusterSets are bound + to workload namespaces; 4) Namespace-scoped Placements specify a slice of + ManagedClusterSets which select a working set of potential ManagedClusters; + 5) Then Placements subselect from that working set using label/claim selection. + \n No ManagedCluster will be selected if no ManagedClusterSet is bound to + the placement namespace. User is able to bind a ManagedClusterSet to a namespace + by creating a ManagedClusterSetBinding in that namespace if they have a + RBAC rule to CREATE on the virtual subresource of `managedclustersets/bind`. + \n A slice of PlacementDecisions with label cluster.open-cluster-management.io/placement={placement + name} will be created to represent the ManagedClusters selected by this + placement. \n If a ManagedCluster is selected and added into the PlacementDecisions, + other components may apply workload on it; once it is removed from the PlacementDecisions, + the workload applied on this ManagedCluster should be evicted accordingly." + type: object + required: + - spec + 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: Spec defines the attributes of Placement. + type: object + properties: + clusterSets: + description: ClusterSets represent the ManagedClusterSets from which + the ManagedClusters are selected. If the slice is empty, ManagedClusters + will be selected from the ManagedClusterSets bound to the placement + namespace, otherwise ManagedClusters will be selected from the intersection + of this slice and the ManagedClusterSets bound to the placement + namespace. + type: array + items: + type: string + numberOfClusters: + description: NumberOfClusters represents the desired number of ManagedClusters + to be selected which meet the placement requirements. 1) If not + specified, all ManagedClusters which meet the placement requirements + (including ClusterSets, and Predicates) will be selected; 2) + Otherwise if the nubmer of ManagedClusters meet the placement requirements + is larger than NumberOfClusters, a random subset with desired + number of ManagedClusters will be selected; 3) If the nubmer of + ManagedClusters meet the placement requirements is equal to NumberOfClusters, all + of them will be selected; 4) If the nubmer of ManagedClusters meet + the placement requirements is less than NumberOfClusters, all + of them will be selected, and the status of condition `PlacementConditionSatisfied` + will be set to false; + type: integer + format: int32 + predicates: + description: Predicates represent a slice of predicates to select + ManagedClusters. The predicates are ORed. + type: array + items: + description: ClusterPredicate represents a predicate to select ManagedClusters. + type: object + properties: + requiredClusterSelector: + description: RequiredClusterSelector represents a selector of + ManagedClusters by label and claim. If specified, 1) Any ManagedCluster, + which does not match the selector, should not be selected + by this ClusterPredicate; 2) If a selected ManagedCluster + (of this ClusterPredicate) ceases to match the selector (e.g. + due to an update) of any ClusterPredicate, it will be eventually + removed from the placement decisions; 3) If a ManagedCluster + (not selected previously) starts to match the selector, it + will either be selected or at least has a chance to be + selected (when NumberOfClusters is specified); + type: object + properties: + claimSelector: + description: ClaimSelector represents a selector of ManagedClusters + by clusterClaims in status + type: object + properties: + matchExpressions: + description: matchExpressions is a list of cluster claim + selector requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + type: array + items: + type: string + labelSelector: + description: LabelSelector represents a selector of ManagedClusters + by label + type: object + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + type: array + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + type: object + required: + - key + - operator + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + type: array + items: + type: string + matchLabels: + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + additionalProperties: + type: string + status: + description: Status represents the current status of the Placement + type: object + properties: + conditions: + description: Conditions contains the different condition statuses + for this Placement. + type: array + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: + \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // +listMapKey=type + \ Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + numberOfSelectedClusters: + description: NumberOfSelectedClusters represents the number of selected + ManagedClusters + type: integer + format: int32 + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/hub/serviceaccount.yaml b/deploy/hub/serviceaccount.yaml new file mode 100644 index 000000000..03c57b55a --- /dev/null +++ b/deploy/hub/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cluster-manager-placement-controller-sa + namespace: open-cluster-management-hub diff --git a/test/e2e/placement_test.go b/test/e2e/placement_test.go new file mode 100644 index 000000000..b5a92fb4d --- /dev/null +++ b/test/e2e/placement_test.go @@ -0,0 +1,216 @@ +package e2e + +import ( + "context" + "fmt" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + clusterapiv1 "github.com/open-cluster-management/api/cluster/v1" + clusterapiv1alpha1 "github.com/open-cluster-management/api/cluster/v1alpha1" + "github.com/open-cluster-management/placement/test/integration/util" +) + +const ( + clusterSetLabel = "cluster.open-cluster-management.io/clusterset" + placementLabel = "cluster.open-cluster-management.io/placement" +) + +var _ = ginkgo.Describe("Placement", func() { + var namespace string + var placementName string + var clusterSet1Name string + var suffix string + var err error + + ginkgo.BeforeEach(func() { + suffix = rand.String(5) + namespace = fmt.Sprintf("ns-%s", suffix) + placementName = fmt.Sprintf("placement-%s", suffix) + clusterSet1Name = fmt.Sprintf("clusterset-%s", suffix) + + // create testing namespace + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + _, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + }) + + ginkgo.AfterEach(func() { + err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), namespace, metav1.DeleteOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + }) + + assertPlacementDecisionCreated := func(placement *clusterapiv1alpha1.Placement) { + ginkgo.By("Check if placementdecision is created") + gomega.Eventually(func() bool { + pdl, err := clusterClient.ClusterV1alpha1().PlacementDecisions(namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: placementLabel + "=" + placement.Name, + }) + if err != nil { + return false + } + if len(pdl.Items) == 0 { + return false + } + for _, pd := range pdl.Items { + if controlled := metav1.IsControlledBy(&pd.ObjectMeta, placement); !controlled { + return false + } + } + return true + }, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue()) + } + + assertNumberOfDecisions := func(placementName string, desiredNOD int) { + ginkgo.By("Check the number of decisions in placementdecisions") + gomega.Eventually(func() bool { + pdl, err := clusterClient.ClusterV1alpha1().PlacementDecisions(namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: placementLabel + "=" + placementName, + }) + if err != nil { + return false + } + if len(pdl.Items) == 0 { + return false + } + actualNOD := 0 + for _, pd := range pdl.Items { + actualNOD += len(pd.Status.Decisions) + } + return actualNOD == desiredNOD + }, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue()) + } + + assertPlacementStatus := func(placementName string, numOfSelectedClusters int, satisfied bool) { + ginkgo.By("Check the status of placement") + gomega.Eventually(func() bool { + placement, err := clusterClient.ClusterV1alpha1().Placements(namespace).Get(context.Background(), placementName, metav1.GetOptions{}) + if err != nil { + return false + } + if satisfied && !util.HasCondition( + placement.Status.Conditions, + clusterapiv1alpha1.PlacementConditionSatisfied, + "AllDecisionsScheduled", + metav1.ConditionTrue, + ) { + return false + } + if !satisfied && !util.HasCondition( + placement.Status.Conditions, + clusterapiv1alpha1.PlacementConditionSatisfied, + "NotAllDecisionsScheduled", + metav1.ConditionFalse, + ) { + return false + } + return placement.Status.NumberOfSelectedClusters == int32(numOfSelectedClusters) + }, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue()) + } + + assertBindingClusterSet := func(clusterSetName string) { + ginkgo.By("Create clusterset/clustersetbinding") + clusterset := &clusterapiv1alpha1.ManagedClusterSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterSetName, + }, + } + _, err = clusterClient.ClusterV1alpha1().ManagedClusterSets().Create(context.Background(), clusterset, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + csb := &clusterapiv1alpha1.ManagedClusterSetBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: clusterSetName, + }, + Spec: clusterapiv1alpha1.ManagedClusterSetBindingSpec{ + ClusterSet: clusterSetName, + }, + } + _, err = clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.Background(), csb, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + } + + assertCreatingClusters := func(clusterSetName string, num int) { + ginkgo.By(fmt.Sprintf("Create %d clusters", num)) + for i := 0; i < num; i++ { + cluster := &clusterapiv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "cluster-", + Labels: map[string]string{ + clusterSetLabel: clusterSetName, + }, + }, + } + _, err = clusterClient.ClusterV1().ManagedClusters().Create(context.Background(), cluster, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + } + } + + assertCreatingPlacement := func(name string, noc *int32, nod int) { + ginkgo.By("Create placement") + placement := &clusterapiv1alpha1.Placement{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: clusterapiv1alpha1.PlacementSpec{ + NumberOfClusters: noc, + }, + } + placement, err = clusterClient.ClusterV1alpha1().Placements(namespace).Create(context.Background(), placement, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + assertPlacementDecisionCreated(placement) + assertNumberOfDecisions(placementName, nod) + if noc != nil { + assertPlacementStatus(placementName, nod, nod == int(*noc)) + } + } + + ginkgo.It("Should schedule successfully", func() { + assertBindingClusterSet(clusterSet1Name) + assertCreatingClusters(clusterSet1Name, 5) + assertCreatingPlacement(placementName, noc(10), 5) + + ginkgo.By("Reduce NOC of the placement") + placement, err := clusterClient.ClusterV1alpha1().Placements(namespace).Get(context.Background(), placementName, metav1.GetOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + noc := int32(6) + placement.Spec.NumberOfClusters = &noc + placement, err = clusterClient.ClusterV1alpha1().Placements(namespace).Update(context.Background(), placement, metav1.UpdateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + assertNumberOfDecisions(placementName, 5) + assertPlacementStatus(placementName, 5, false) + + ginkgo.By("Delete placement") + err = clusterClient.ClusterV1alpha1().Placements(namespace).Delete(context.TODO(), placementName, metav1.DeleteOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + ginkgo.By("Check if placementdecisions are deleted as well") + gomega.Eventually(func() bool { + placementDecisions, err := clusterClient.ClusterV1alpha1().PlacementDecisions(namespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", placementLabel, placementName), + }) + if err != nil { + return false + } + + return len(placementDecisions.Items) == 0 + }, eventuallyTimeout*5, eventuallyInterval*5).Should(gomega.BeTrue()) + }) +}) + +func noc(n int) *int32 { + noc := int32(n) + return &noc +} diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go new file mode 100644 index 000000000..a6b5afe1a --- /dev/null +++ b/test/e2e/suite_test.go @@ -0,0 +1,47 @@ +package e2e + +import ( + "os" + "testing" + + ginkgo "github.com/onsi/ginkgo" + gomega "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + clusterclient "github.com/open-cluster-management/api/client/cluster/clientset/versioned" +) + +const ( + eventuallyTimeout = 30 // seconds + eventuallyInterval = 1 // seconds +) + +func TestE2E(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "E2E Suite") +} + +var ( + kubeClient kubernetes.Interface + clusterClient clusterclient.Interface + restConfig *rest.Config +) + +var _ = ginkgo.BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(ginkgo.GinkgoWriter), zap.UseDevMode(true))) + kubeconfig := os.Getenv("KUBECONFIG") + + var err error + restConfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + kubeClient, err = kubernetes.NewForConfig(restConfig) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + clusterClient, err = clusterclient.NewForConfig(restConfig) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) +}) diff --git a/vendor/github.com/go-logr/zapr/.gitignore b/vendor/github.com/go-logr/zapr/.gitignore new file mode 100644 index 000000000..5ba77727f --- /dev/null +++ b/vendor/github.com/go-logr/zapr/.gitignore @@ -0,0 +1,3 @@ +*~ +*.swp +/vendor diff --git a/vendor/github.com/go-logr/zapr/Gopkg.lock b/vendor/github.com/go-logr/zapr/Gopkg.lock new file mode 100644 index 000000000..8da0a8f76 --- /dev/null +++ b/vendor/github.com/go-logr/zapr/Gopkg.lock @@ -0,0 +1,52 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + digest = "1:edd2fa4578eb086265db78a9201d15e76b298dfd0d5c379da83e9c61712cf6df" + name = "github.com/go-logr/logr" + packages = ["."] + pruneopts = "UT" + revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e" + version = "v0.1.0" + +[[projects]] + digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d" + name = "go.uber.org/atomic" + packages = ["."] + pruneopts = "UT" + revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" + version = "v1.3.2" + +[[projects]] + digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a" + name = "go.uber.org/multierr" + packages = ["."] + pruneopts = "UT" + revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" + version = "v1.1.0" + +[[projects]] + digest = "1:9580b1b079114140ade8cec957685344d14f00119e0241f6b369633cb346eeb3" + name = "go.uber.org/zap" + packages = [ + ".", + "buffer", + "internal/bufferpool", + "internal/color", + "internal/exit", + "zapcore", + ] + pruneopts = "UT" + revision = "eeedf312bc6c57391d84767a4cd413f02a917974" + version = "v1.8.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "github.com/go-logr/logr", + "go.uber.org/zap", + "go.uber.org/zap/zapcore", + ] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/github.com/go-logr/zapr/Gopkg.toml b/vendor/github.com/go-logr/zapr/Gopkg.toml new file mode 100644 index 000000000..ae475d72e --- /dev/null +++ b/vendor/github.com/go-logr/zapr/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/go-logr/logr" + version = "0.1.0" + +[[constraint]] + name = "go.uber.org/zap" + version = "1.8.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/go-logr/zapr/LICENSE b/vendor/github.com/go-logr/zapr/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/vendor/github.com/go-logr/zapr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/go-logr/zapr/README.md b/vendor/github.com/go-logr/zapr/README.md new file mode 100644 index 000000000..548470ee1 --- /dev/null +++ b/vendor/github.com/go-logr/zapr/README.md @@ -0,0 +1,45 @@ +Zapr :zap: +========== + +A [logr](https://github.com/go-logr/logr) implementation using +[Zap](https://github.com/uber-go/zap). + +Usage +----- + +```go +import ( + "fmt" + + "go.uber.org/zap" + "github.com/go-logr/logr" + "github.com/go-logr/zapr" +) + +func main() { + var log logr.Logger + + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + + log.Info("Logr in action!", "the answer", 42) +} +``` + +Implementation Details +---------------------- + +For the most part, concepts in Zap correspond directly with those in logr. + +Unlike Zap, all fields *must* be in the form of suggared fields -- +it's illegal to pass a strongly-typed Zap field in a key position to any +of the logging methods (`Log`, `Error`). + +Levels in logr correspond to custom debug levels in Zap. Any given level +in logr is represents by its inverse in Zap (`zapLevel = -1*logrLevel`). + +For example `V(2)` is equivalent to log level -2 in Zap, while `V(1)` is +equivalent to Zap's `DebugLevel`. diff --git a/vendor/github.com/go-logr/zapr/go.mod b/vendor/github.com/go-logr/zapr/go.mod new file mode 100644 index 000000000..f7d7e256c --- /dev/null +++ b/vendor/github.com/go-logr/zapr/go.mod @@ -0,0 +1,10 @@ +module github.com/go-logr/zapr + +go 1.12 + +require ( + github.com/go-logr/logr v0.2.0 + go.uber.org/atomic v1.3.2 + go.uber.org/multierr v1.1.0 + go.uber.org/zap v1.8.0 +) diff --git a/vendor/github.com/go-logr/zapr/zapr.go b/vendor/github.com/go-logr/zapr/zapr.go new file mode 100644 index 000000000..09a074d07 --- /dev/null +++ b/vendor/github.com/go-logr/zapr/zapr.go @@ -0,0 +1,167 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Copyright 2018 Solly Ross +// +// 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 zapr defines an implementation of the github.com/go-logr/logr +// interfaces built on top of Zap (go.uber.org/zap). +// +// Usage +// +// A new logr.Logger can be constructed from an existing zap.Logger using +// the NewLogger function: +// +// log := zapr.NewLogger(someZapLogger) +// +// Implementation Details +// +// For the most part, concepts in Zap correspond directly with those in +// logr. +// +// Unlike Zap, all fields *must* be in the form of sugared fields -- +// it's illegal to pass a strongly-typed Zap field in a key position +// to any of the log methods. +// +// Levels in logr correspond to custom debug levels in Zap. Any given level +// in logr is represents by its inverse in zap (`zapLevel = -1*logrLevel`). +// For example V(2) is equivalent to log level -2 in Zap, while V(1) is +// equivalent to Zap's DebugLevel. +package zapr + +import ( + "github.com/go-logr/logr" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// NB: right now, we always use the equivalent of sugared logging. +// This is necessary, since logr doesn't define non-suggared types, +// and using zap-specific non-suggared types would make uses tied +// directly to Zap. + +// zapLogger is a logr.Logger that uses Zap to log. The level has already been +// converted to a Zap level, which is to say that `logrLevel = -1*zapLevel`. +type zapLogger struct { + // NB: this looks very similar to zap.SugaredLogger, but + // deals with our desire to have multiple verbosity levels. + l *zap.Logger + lvl zapcore.Level +} + +// handleFields converts a bunch of arbitrary key-value pairs into Zap fields. It takes +// additional pre-converted Zap fields, for use with automatically attached fields, like +// `error`. +func handleFields(l *zap.Logger, args []interface{}, additional ...zap.Field) []zap.Field { + // a slightly modified version of zap.SugaredLogger.sweetenFields + if len(args) == 0 { + // fast-return if we have no suggared fields. + return additional + } + + // unlike Zap, we can be pretty sure users aren't passing structured + // fields (since logr has no concept of that), so guess that we need a + // little less space. + fields := make([]zap.Field, 0, len(args)/2+len(additional)) + for i := 0; i < len(args); { + // check just in case for strongly-typed Zap fields, which is illegal (since + // it breaks implementation agnosticism), so we can give a better error message. + if _, ok := args[i].(zap.Field); ok { + l.DPanic("strongly-typed Zap Field passed to logr", zap.Any("zap field", args[i])) + break + } + + // make sure this isn't a mismatched key + if i == len(args)-1 { + l.DPanic("odd number of arguments passed as key-value pairs for logging", zap.Any("ignored key", args[i])) + break + } + + // process a key-value pair, + // ensuring that the key is a string + key, val := args[i], args[i+1] + keyStr, isString := key.(string) + if !isString { + // if the key isn't a string, DPanic and stop logging + l.DPanic("non-string key argument passed to logging, ignoring all later arguments", zap.Any("invalid key", key)) + break + } + + fields = append(fields, zap.Any(keyStr, val)) + i += 2 + } + + return append(fields, additional...) +} + +func (zl *zapLogger) Enabled() bool { + return zl.l.Core().Enabled(zl.lvl) +} + +func (zl *zapLogger) Info(msg string, keysAndVals ...interface{}) { + if checkedEntry := zl.l.Check(zl.lvl, msg); checkedEntry != nil { + checkedEntry.Write(handleFields(zl.l, keysAndVals)...) + } +} + +func (zl *zapLogger) Error(err error, msg string, keysAndVals ...interface{}) { + if checkedEntry := zl.l.Check(zap.ErrorLevel, msg); checkedEntry != nil { + checkedEntry.Write(handleFields(zl.l, keysAndVals, zap.Error(err))...) + } +} + +func (zl *zapLogger) V(level int) logr.Logger { + return &zapLogger{ + lvl: zl.lvl - zapcore.Level(level), + l: zl.l, + } +} + +func (zl *zapLogger) WithValues(keysAndValues ...interface{}) logr.Logger { + newLogger := zl.l.With(handleFields(zl.l, keysAndValues)...) + return newLoggerWithExtraSkip(newLogger, 0) +} + +func (zl *zapLogger) WithName(name string) logr.Logger { + newLogger := zl.l.Named(name) + return newLoggerWithExtraSkip(newLogger, 0) +} + +// newLoggerWithExtraSkip allows creation of loggers with variable levels of callstack skipping +func newLoggerWithExtraSkip(l *zap.Logger, callerSkip int) logr.Logger { + log := l.WithOptions(zap.AddCallerSkip(callerSkip)) + return &zapLogger{ + l: log, + lvl: zap.InfoLevel, + } +} + +// NewLogger creates a new logr.Logger using the given Zap Logger to log. +func NewLogger(l *zap.Logger) logr.Logger { + // creates a new logger skipping one level of callstack + return newLoggerWithExtraSkip(l, 1) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9ddb9d285..752080de4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -30,6 +30,8 @@ github.com/fsnotify/fsnotify github.com/ghodss/yaml # github.com/go-logr/logr v0.4.0 github.com/go-logr/logr +# github.com/go-logr/zapr v0.2.0 +github.com/go-logr/zapr # github.com/go-openapi/jsonpointer v0.19.3 github.com/go-openapi/jsonpointer # github.com/go-openapi/jsonreference v0.19.3 @@ -889,6 +891,7 @@ sigs.k8s.io/controller-runtime/pkg/internal/testing/integration sigs.k8s.io/controller-runtime/pkg/internal/testing/integration/addr sigs.k8s.io/controller-runtime/pkg/internal/testing/integration/internal sigs.k8s.io/controller-runtime/pkg/log +sigs.k8s.io/controller-runtime/pkg/log/zap # sigs.k8s.io/structured-merge-diff/v4 v4.1.0 sigs.k8s.io/structured-merge-diff/v4/fieldpath sigs.k8s.io/structured-merge-diff/v4/merge diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go new file mode 100644 index 000000000..333965507 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go @@ -0,0 +1,130 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package zap contains helpers for setting up a new logr.Logger instance +// using the Zap logging framework. +package zap + +import ( + "flag" + "fmt" + "strconv" + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var levelStrings = map[string]zapcore.Level{ + "debug": zap.DebugLevel, + "info": zap.InfoLevel, + "error": zap.ErrorLevel, +} + +var stackLevelStrings = map[string]zapcore.Level{ + "info": zap.InfoLevel, + "error": zap.ErrorLevel, + "panic": zap.PanicLevel, +} + +type encoderFlag struct { + setFunc func(NewEncoderFunc) + value string +} + +var _ flag.Value = &encoderFlag{} + +func (ev *encoderFlag) String() string { + return ev.value +} + +func (ev *encoderFlag) Type() string { + return "encoder" +} + +func (ev *encoderFlag) Set(flagValue string) error { + val := strings.ToLower(flagValue) + switch val { + case "json": + ev.setFunc(newJSONEncoder) + case "console": + ev.setFunc(newConsoleEncoder) + default: + return fmt.Errorf("invalid encoder value \"%s\"", flagValue) + } + ev.value = flagValue + return nil +} + +type levelFlag struct { + setFunc func(zapcore.LevelEnabler) + value string +} + +var _ flag.Value = &levelFlag{} + +func (ev *levelFlag) Set(flagValue string) error { + level, validLevel := levelStrings[strings.ToLower(flagValue)] + if !validLevel { + logLevel, err := strconv.Atoi(flagValue) + if err != nil { + return fmt.Errorf("invalid log level \"%s\"", flagValue) + } + if logLevel > 0 { + intLevel := -1 * logLevel + ev.setFunc(zap.NewAtomicLevelAt(zapcore.Level(int8(intLevel)))) + } else { + return fmt.Errorf("invalid log level \"%s\"", flagValue) + } + } else { + ev.setFunc(zap.NewAtomicLevelAt(level)) + } + ev.value = flagValue + return nil +} + +func (ev *levelFlag) String() string { + return ev.value +} + +func (ev *levelFlag) Type() string { + return "level" +} + +type stackTraceFlag struct { + setFunc func(zapcore.LevelEnabler) + value string +} + +var _ flag.Value = &stackTraceFlag{} + +func (ev *stackTraceFlag) Set(flagValue string) error { + level, validLevel := stackLevelStrings[strings.ToLower(flagValue)] + if !validLevel { + return fmt.Errorf("invalid stacktrace level \"%s\"", flagValue) + } + ev.setFunc(zap.NewAtomicLevelAt(level)) + ev.value = flagValue + return nil +} + +func (ev *stackTraceFlag) String() string { + return ev.value +} + +func (ev *stackTraceFlag) Type() string { + return "level" +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go new file mode 100644 index 000000000..e37df1aed --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go @@ -0,0 +1,129 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package zap + +import ( + "fmt" + + "go.uber.org/zap/buffer" + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +// KubeAwareEncoder is a Kubernetes-aware Zap Encoder. +// Instead of trying to force Kubernetes objects to implement +// ObjectMarshaller, we just implement a wrapper around a normal +// ObjectMarshaller that checks for Kubernetes objects. +type KubeAwareEncoder struct { + // Encoder is the zapcore.Encoder that this encoder delegates to + zapcore.Encoder + + // Verbose controls whether or not the full object is printed. + // If false, only name, namespace, api version, and kind are printed. + // Otherwise, the full object is logged. + Verbose bool +} + +// namespacedNameWrapper is a zapcore.ObjectMarshaler for Kubernetes NamespacedName +type namespacedNameWrapper struct { + types.NamespacedName +} + +func (w namespacedNameWrapper) MarshalLogObject(enc zapcore.ObjectEncoder) error { + if w.Namespace != "" { + enc.AddString("namespace", w.Namespace) + } + + enc.AddString("name", w.Name) + + return nil +} + +// kubeObjectWrapper is a zapcore.ObjectMarshaler for Kubernetes objects. +type kubeObjectWrapper struct { + obj runtime.Object +} + +// MarshalLogObject implements zapcore.ObjectMarshaler +func (w kubeObjectWrapper) MarshalLogObject(enc zapcore.ObjectEncoder) error { + // TODO(directxman12): log kind and apiversion if not set explicitly (common case) + // -- needs an a scheme to convert to the GVK. + gvk := w.obj.GetObjectKind().GroupVersionKind() + if gvk.Version != "" { + enc.AddString("apiVersion", gvk.GroupVersion().String()) + enc.AddString("kind", gvk.Kind) + } + + objMeta, err := meta.Accessor(w.obj) + if err != nil { + return fmt.Errorf("got runtime.Object without object metadata: %v", w.obj) + } + + ns := objMeta.GetNamespace() + if ns != "" { + enc.AddString("namespace", ns) + } + enc.AddString("name", objMeta.GetName()) + + return nil +} + +// NB(directxman12): can't just override AddReflected, since the encoder calls AddReflected on itself directly + +// Clone implements zapcore.Encoder +func (k *KubeAwareEncoder) Clone() zapcore.Encoder { + return &KubeAwareEncoder{ + Encoder: k.Encoder.Clone(), + } +} + +// EncodeEntry implements zapcore.Encoder +func (k *KubeAwareEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + if k.Verbose { + // Kubernetes objects implement fmt.Stringer, so if we + // want verbose output, just delegate to that. + return k.Encoder.EncodeEntry(entry, fields) + } + + for i, field := range fields { + // intercept stringer fields that happen to be Kubernetes runtime.Object or + // types.NamespacedName values (Kubernetes runtime.Objects commonly + // implement String, apparently). + // *unstructured.Unstructured does NOT implement fmt.Striger interface. + // We have handle it specially. + if field.Type == zapcore.StringerType || field.Type == zapcore.ReflectType { + switch val := field.Interface.(type) { + case runtime.Object: + fields[i] = zapcore.Field{ + Type: zapcore.ObjectMarshalerType, + Key: field.Key, + Interface: kubeObjectWrapper{obj: val}, + } + case types.NamespacedName: + fields[i] = zapcore.Field{ + Type: zapcore.ObjectMarshalerType, + Key: field.Key, + Interface: namespacedNameWrapper{NamespacedName: val}, + } + } + } + } + + return k.Encoder.EncodeEntry(entry, fields) +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go new file mode 100644 index 000000000..8aff63ee8 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go @@ -0,0 +1,283 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package zap contains helpers for setting up a new logr.Logger instance +// using the Zap logging framework. +package zap + +import ( + "flag" + "io" + "os" + "time" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// EncoderConfigOption is a function that can modify a `zapcore.EncoderConfig`. +type EncoderConfigOption func(*zapcore.EncoderConfig) + +// NewEncoderFunc is a function that creates an Encoder using the provided EncoderConfigOptions. +type NewEncoderFunc func(...EncoderConfigOption) zapcore.Encoder + +// New returns a brand new Logger configured with Opts. It +// uses KubeAwareEncoder which adds Type information and +// Namespace/Name to the log. +func New(opts ...Opts) logr.Logger { + return zapr.NewLogger(NewRaw(opts...)) +} + +// Opts allows to manipulate Options +type Opts func(*Options) + +// UseDevMode sets the logger to use (or not use) development mode (more +// human-readable output, extra stack traces and logging information, etc). +// See Options.Development +func UseDevMode(enabled bool) Opts { + return func(o *Options) { + o.Development = enabled + } +} + +// WriteTo configures the logger to write to the given io.Writer, instead of standard error. +// See Options.DestWriter +func WriteTo(out io.Writer) Opts { + return func(o *Options) { + o.DestWriter = out + } +} + +// Encoder configures how the logger will encode the output e.g JSON or console. +// See Options.Encoder +func Encoder(encoder zapcore.Encoder) func(o *Options) { + return func(o *Options) { + o.Encoder = encoder + } +} + +// JSONEncoder configures the logger to use a JSON Encoder +func JSONEncoder(opts ...EncoderConfigOption) func(o *Options) { + return func(o *Options) { + o.Encoder = newJSONEncoder(opts...) + } +} + +func newJSONEncoder(opts ...EncoderConfigOption) zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + for _, opt := range opts { + opt(&encoderConfig) + } + return zapcore.NewJSONEncoder(encoderConfig) +} + +// ConsoleEncoder configures the logger to use a Console encoder +func ConsoleEncoder(opts ...EncoderConfigOption) func(o *Options) { + return func(o *Options) { + o.Encoder = newConsoleEncoder(opts...) + } +} + +func newConsoleEncoder(opts ...EncoderConfigOption) zapcore.Encoder { + encoderConfig := zap.NewDevelopmentEncoderConfig() + for _, opt := range opts { + opt(&encoderConfig) + } + return zapcore.NewConsoleEncoder(encoderConfig) +} + +// Level sets the the minimum enabled logging level e.g Debug, Info +// See Options.Level +func Level(level zapcore.LevelEnabler) func(o *Options) { + return func(o *Options) { + o.Level = level + } +} + +// StacktraceLevel configures the logger to record a stack trace for all messages at +// or above a given level. +// See Options.StacktraceLevel +func StacktraceLevel(stacktraceLevel zapcore.LevelEnabler) func(o *Options) { + return func(o *Options) { + o.StacktraceLevel = stacktraceLevel + } +} + +// RawZapOpts allows appending arbitrary zap.Options to configure the underlying zap logger. +// See Options.ZapOpts +func RawZapOpts(zapOpts ...zap.Option) func(o *Options) { + return func(o *Options) { + o.ZapOpts = append(o.ZapOpts, zapOpts...) + } +} + +// Options contains all possible settings +type Options struct { + // Development configures the logger to use a Zap development config + // (stacktraces on warnings, no sampling), otherwise a Zap production + // config will be used (stacktraces on errors, sampling). + Development bool + // Encoder configures how Zap will encode the output. Defaults to + // console when Development is true and JSON otherwise + Encoder zapcore.Encoder + // EncoderConfigOptions can modify the EncoderConfig needed to initialize an Encoder. + // See https://godoc.org/go.uber.org/zap/zapcore#EncoderConfig for the list of options + // that can be configured. + // Note that the EncoderConfigOptions are not applied when the Encoder option is already set. + EncoderConfigOptions []EncoderConfigOption + // NewEncoder configures Encoder using the provided EncoderConfigOptions. + // Note that the NewEncoder function is not used when the Encoder option is already set. + NewEncoder NewEncoderFunc + // DestWriter controls the destination of the log output. Defaults to + // os.Stderr. + DestWriter io.Writer + // DestWritter controls the destination of the log output. Defaults to + // os.Stderr. + // + // Deprecated: Use DestWriter instead + DestWritter io.Writer + // Level configures the verbosity of the logging. Defaults to Debug when + // Development is true and Info otherwise + Level zapcore.LevelEnabler + // StacktraceLevel is the level at and above which stacktraces will + // be recorded for all messages. Defaults to Warn when Development + // is true and Error otherwise + StacktraceLevel zapcore.LevelEnabler + // ZapOpts allows passing arbitrary zap.Options to configure on the + // underlying Zap logger. + ZapOpts []zap.Option +} + +// addDefaults adds defaults to the Options +func (o *Options) addDefaults() { + if o.DestWriter == nil && o.DestWritter == nil { + o.DestWriter = os.Stderr + } else if o.DestWriter == nil && o.DestWritter != nil { + // while misspelled DestWritter is deprecated but still not removed + o.DestWriter = o.DestWritter + } + + if o.Development { + if o.NewEncoder == nil { + o.NewEncoder = newConsoleEncoder + } + if o.Level == nil { + lvl := zap.NewAtomicLevelAt(zap.DebugLevel) + o.Level = &lvl + } + if o.StacktraceLevel == nil { + lvl := zap.NewAtomicLevelAt(zap.WarnLevel) + o.StacktraceLevel = &lvl + } + o.ZapOpts = append(o.ZapOpts, zap.Development()) + + } else { + if o.NewEncoder == nil { + o.NewEncoder = newJSONEncoder + } + if o.Level == nil { + lvl := zap.NewAtomicLevelAt(zap.InfoLevel) + o.Level = &lvl + } + if o.StacktraceLevel == nil { + lvl := zap.NewAtomicLevelAt(zap.ErrorLevel) + o.StacktraceLevel = &lvl + } + // Disable sampling for increased Debug levels. Otherwise, this will + // cause index out of bounds errors in the sampling code. + if !o.Level.Enabled(zapcore.Level(-2)) { + o.ZapOpts = append(o.ZapOpts, + zap.WrapCore(func(core zapcore.Core) zapcore.Core { + return zapcore.NewSampler(core, time.Second, 100, 100) + })) + } + } + if o.Encoder == nil { + o.Encoder = o.NewEncoder(o.EncoderConfigOptions...) + } + o.ZapOpts = append(o.ZapOpts, zap.AddStacktrace(o.StacktraceLevel)) +} + +// NewRaw returns a new zap.Logger configured with the passed Opts +// or their defaults. It uses KubeAwareEncoder which adds Type +// information and Namespace/Name to the log. +func NewRaw(opts ...Opts) *zap.Logger { + o := &Options{} + for _, opt := range opts { + opt(o) + } + o.addDefaults() + + // this basically mimics NewConfig, but with a custom sink + sink := zapcore.AddSync(o.DestWriter) + + o.ZapOpts = append(o.ZapOpts, zap.AddCallerSkip(1), zap.ErrorOutput(sink)) + log := zap.New(zapcore.NewCore(&KubeAwareEncoder{Encoder: o.Encoder, Verbose: o.Development}, sink, o.Level)) + log = log.WithOptions(o.ZapOpts...) + return log +} + +// BindFlags will parse the given flagset for zap option flags and set the log options accordingly +// zap-devel: Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn) +// Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) +// zap-encoder: Zap log encoding (one of 'json' or 'console') +// zap-log-level: Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', +// or any integer value > 0 which corresponds to custom debug levels of increasing verbosity") +// zap-stacktrace-level: Zap Level at and above which stacktraces are captured (one of 'info', 'error' or 'panic') +func (o *Options) BindFlags(fs *flag.FlagSet) { + + // Set Development mode value + fs.BoolVar(&o.Development, "zap-devel", o.Development, + "Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). "+ + "Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error)") + + // Set Encoder value + var encVal encoderFlag + encVal.setFunc = func(fromFlag NewEncoderFunc) { + o.NewEncoder = fromFlag + } + fs.Var(&encVal, "zap-encoder", "Zap log encoding (one of 'json' or 'console')") + + // Set the Log Level + var levelVal levelFlag + levelVal.setFunc = func(fromFlag zapcore.LevelEnabler) { + o.Level = fromFlag + } + fs.Var(&levelVal, "zap-log-level", + "Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', "+ + "or any integer value > 0 which corresponds to custom debug levels of increasing verbosity") + + // Set the StrackTrace Level + var stackVal stackTraceFlag + stackVal.setFunc = func(fromFlag zapcore.LevelEnabler) { + o.StacktraceLevel = fromFlag + } + fs.Var(&stackVal, "zap-stacktrace-level", + "Zap Level at and above which stacktraces are captured (one of 'info', 'error', 'panic').") +} + +// UseFlagOptions configures the logger to use the Options set by parsing zap option flags from the CLI. +// opts := zap.Options{} +// opts.BindFlags(flag.CommandLine) +// flag.Parse() +// log := zap.New(zap.UseFlagOptions(&opts)) +func UseFlagOptions(in *Options) Opts { + return func(o *Options) { + *o = *in + } +}