mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-05-21 16:43:48 +00:00
@@ -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}
|
||||
|
||||
33
Makefile
33
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
|
||||
|
||||
27
deploy/hub/clusterrole.yaml
Normal file
27
deploy/hub/clusterrole.yaml
Normal 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"]
|
||||
12
deploy/hub/clusterrolebinding.yaml
Normal file
12
deploy/hub/clusterrolebinding.yaml
Normal 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
|
||||
70
deploy/hub/deployment.yaml
Normal file
70
deploy/hub/deployment.yaml
Normal 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
|
||||
44
deploy/hub/kustomization.yaml
Normal file
44
deploy/hub/kustomization.yaml
Normal 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
|
||||
241
deploy/hub/managedclusters.crd.yaml
Normal file
241
deploy/hub/managedclusters.crd.yaml
Normal 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: []
|
||||
57
deploy/hub/managedclustersetbindings.crd.yaml
Normal file
57
deploy/hub/managedclustersetbindings.crd.yaml
Normal 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: []
|
||||
131
deploy/hub/managedclustersets.crd.yaml
Normal file
131
deploy/hub/managedclustersets.crd.yaml
Normal 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: []
|
||||
4
deploy/hub/namespace.yaml
Normal file
4
deploy/hub/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: open-cluster-management-hub
|
||||
76
deploy/hub/placementdecisions.crd.yaml
Normal file
76
deploy/hub/placementdecisions.crd.yaml
Normal 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: []
|
||||
274
deploy/hub/placements.crd.yaml
Normal file
274
deploy/hub/placements.crd.yaml
Normal 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: []
|
||||
5
deploy/hub/serviceaccount.yaml
Normal file
5
deploy/hub/serviceaccount.yaml
Normal 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
216
test/e2e/placement_test.go
Normal 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
47
test/e2e/suite_test.go
Normal 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
3
vendor/github.com/go-logr/zapr/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*~
|
||||
*.swp
|
||||
/vendor
|
||||
52
vendor/github.com/go-logr/zapr/Gopkg.lock
generated
vendored
Normal file
52
vendor/github.com/go-logr/zapr/Gopkg.lock
generated
vendored
Normal 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
38
vendor/github.com/go-logr/zapr/Gopkg.toml
generated
vendored
Normal 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
201
vendor/github.com/go-logr/zapr/LICENSE
generated
vendored
Normal 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
45
vendor/github.com/go-logr/zapr/README.md
generated
vendored
Normal 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
10
vendor/github.com/go-logr/zapr/go.mod
generated
vendored
Normal 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
167
vendor/github.com/go-logr/zapr/zapr.go
generated
vendored
Normal 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
3
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
130
vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go
generated
vendored
Normal file
130
vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go
generated
vendored
Normal 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"
|
||||
}
|
||||
129
vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go
generated
vendored
Normal file
129
vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go
generated
vendored
Normal 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)
|
||||
}
|
||||
283
vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go
generated
vendored
Normal file
283
vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user