add e2e test cases

Signed-off-by: Yang Le <yangle@redhat.com>
This commit is contained in:
Yang Le
2021-05-24 18:07:34 +08:00
parent 47127df16c
commit cf967a313f
26 changed files with 2301 additions and 0 deletions

View File

@@ -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}

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: open-cluster-management-hub

View File

@@ -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: []

View File

@@ -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: []

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: cluster-manager-placement-controller-sa
namespace: open-cluster-management-hub

216
test/e2e/placement_test.go Normal file
View File

@@ -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
}

47
test/e2e/suite_test.go Normal file
View File

@@ -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())
})

3
vendor/github.com/go-logr/zapr/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
*~
*.swp
/vendor

52
vendor/github.com/go-logr/zapr/Gopkg.lock generated vendored Normal file
View File

@@ -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

38
vendor/github.com/go-logr/zapr/Gopkg.toml generated vendored Normal file
View File

@@ -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

201
vendor/github.com/go-logr/zapr/LICENSE generated vendored Normal file
View File

@@ -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.

45
vendor/github.com/go-logr/zapr/README.md generated vendored Normal file
View File

@@ -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`.

10
vendor/github.com/go-logr/zapr/go.mod generated vendored Normal file
View File

@@ -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
)

167
vendor/github.com/go-logr/zapr/zapr.go generated vendored Normal file
View File

@@ -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)
}

3
vendor/modules.txt vendored
View File

@@ -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

View File

@@ -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"
}

View File

@@ -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)
}

View File

@@ -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 New<type>Config, 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
}
}