diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e8e3a28a592..e48e95f33bc 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,6 +2,12 @@ **Is this a request for help?** (If yes, you should use our troubleshooting guide and community support channels, see http://kubernetes.io/docs/troubleshooting/.): +**Note:** Please file issues for subcomponents under the appropriate repo + +| Component | Repo | +| --------- | ------------------------------------------------------------------ | +| kubectl | [kubernetes/kubectl](https://github.com/kubernetes/kubectl/issues/new) | +| kubeadm | [kubernetes/kubeadm](https://github.com/kubernetes/kubeadm/issues/new) | **What keywords did you search in Kubernetes issues before filing this one?** (If you have found any duplicates, you should instead reply there.): diff --git a/cluster/gce/util.sh b/cluster/gce/util.sh index 547f0085f95..a6d497b7306 100755 --- a/cluster/gce/util.sh +++ b/cluster/gce/util.sh @@ -1203,14 +1203,14 @@ function create-cluster-autoscaler-mig-config() { # Each MIG must have at least one node, so the min number of nodes # must be greater or equal to the number of migs. - if [[ ${AUTOSCALER_MIN_NODES} < ${NUM_MIGS} ]]; then + if [[ ${AUTOSCALER_MIN_NODES} -lt ${NUM_MIGS} ]]; then echo "AUTOSCALER_MIN_NODES must be greater or equal ${NUM_MIGS}" exit 2 fi # Each MIG must have at least one node, so the min number of nodes # must be greater or equal to the number of migs. - if [[ ${AUTOSCALER_MAX_NODES} < ${NUM_MIGS} ]]; then + if [[ ${AUTOSCALER_MAX_NODES} -lt ${NUM_MIGS} ]]; then echo "AUTOSCALER_MAX_NODES must be greater or equal ${NUM_MIGS}" exit 2 fi diff --git a/cmd/cloud-controller-manager/app/controllermanager.go b/cmd/cloud-controller-manager/app/controllermanager.go index 5cc76b5b795..e2defbcb917 100644 --- a/cmd/cloud-controller-manager/app/controllermanager.go +++ b/cmd/cloud-controller-manager/app/controllermanager.go @@ -199,6 +199,12 @@ func StartControllers(s *options.CloudControllerManagerServer, kubeconfig *restc client := func(serviceAccountName string) clientset.Interface { return rootClientBuilder.ClientOrDie(serviceAccountName) } + + if cloud != nil { + // Initialize the cloud provider with a reference to the clientBuilder + cloud.Initialize(clientBuilder) + } + versionedClient := client("shared-informers") sharedInformers := informers.NewSharedInformerFactory(versionedClient, resyncPeriod(s)()) diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index b671a78ee65..d93e5b64aac 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -54,6 +54,7 @@ go_library( "//plugin/pkg/admission/namespace/autoprovision:go_default_library", "//plugin/pkg/admission/namespace/exists:go_default_library", "//plugin/pkg/admission/namespace/lifecycle:go_default_library", + "//plugin/pkg/admission/noderestriction:go_default_library", "//plugin/pkg/admission/persistentvolume/label:go_default_library", "//plugin/pkg/admission/podnodeselector:go_default_library", "//plugin/pkg/admission/podpreset:go_default_library", diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index f5d524ab6d8..0ddd92566ad 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -37,6 +37,7 @@ import ( _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision" _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists" _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/lifecycle" + _ "k8s.io/kubernetes/plugin/pkg/admission/noderestriction" _ "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label" _ "k8s.io/kubernetes/plugin/pkg/admission/podnodeselector" _ "k8s.io/kubernetes/plugin/pkg/admission/podpreset" diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index c6fab913f97..9f347136ee0 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -451,6 +451,11 @@ func StartControllers(controllers map[string]InitFunc, s *options.CMServer, root return fmt.Errorf("cloud provider could not be initialized: %v", err) } + if cloud != nil { + // Initialize the cloud provider with a reference to the clientBuilder + cloud.Initialize(clientBuilder) + } + if ctx.IsControllerEnabled(nodeControllerName) { _, clusterCIDR, err := net.ParseCIDR(s.ClusterCIDR) if err != nil { diff --git a/cmd/libs/go2idl/client-gen/BUILD b/cmd/libs/go2idl/client-gen/BUILD index 81dd0f7b2a1..6a13076b0fb 100644 --- a/cmd/libs/go2idl/client-gen/BUILD +++ b/cmd/libs/go2idl/client-gen/BUILD @@ -41,6 +41,7 @@ filegroup( ":package-srcs", "//cmd/libs/go2idl/client-gen/args:all-srcs", "//cmd/libs/go2idl/client-gen/generators:all-srcs", + "//cmd/libs/go2idl/client-gen/path:all-srcs", "//cmd/libs/go2idl/client-gen/test_apis/testgroup:all-srcs", "//cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset:all-srcs", "//cmd/libs/go2idl/client-gen/types:all-srcs", diff --git a/cmd/libs/go2idl/client-gen/generators/BUILD b/cmd/libs/go2idl/client-gen/generators/BUILD index dd089164209..72b1eb726b1 100644 --- a/cmd/libs/go2idl/client-gen/generators/BUILD +++ b/cmd/libs/go2idl/client-gen/generators/BUILD @@ -22,6 +22,7 @@ go_library( "//cmd/libs/go2idl/client-gen/args:go_default_library", "//cmd/libs/go2idl/client-gen/generators/fake:go_default_library", "//cmd/libs/go2idl/client-gen/generators/scheme:go_default_library", + "//cmd/libs/go2idl/client-gen/path:go_default_library", "//cmd/libs/go2idl/client-gen/types:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/gengo/args:go_default_library", diff --git a/cmd/libs/go2idl/client-gen/generators/client_generator.go b/cmd/libs/go2idl/client-gen/generators/client_generator.go index bb90f0ce0d3..170dedf7797 100644 --- a/cmd/libs/go2idl/client-gen/generators/client_generator.go +++ b/cmd/libs/go2idl/client-gen/generators/client_generator.go @@ -29,6 +29,7 @@ import ( clientgenargs "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/args" "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/generators/fake" "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/generators/scheme" + "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/path" clientgentypes "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/types" "github.com/golang/glog" @@ -275,7 +276,8 @@ func Packages(context *generator.Context, arguments *args.GeneratorArgs) generat gvToTypes := map[clientgentypes.GroupVersion][]*types.Type{} for gv, inputDir := range customArgs.GroupVersionToInputPath { - p := context.Universe.Package(inputDir) + // Package are indexed with the vendor prefix stripped + p := context.Universe.Package(path.Vendorless(inputDir)) for n, t := range p.Types { // filter out types which are not included in user specified overrides. typesOverride, ok := includedTypesOverrides[gv] diff --git a/cmd/libs/go2idl/client-gen/generators/fake/BUILD b/cmd/libs/go2idl/client-gen/generators/fake/BUILD index cad1c60c1a6..08ad1ac1daa 100644 --- a/cmd/libs/go2idl/client-gen/generators/fake/BUILD +++ b/cmd/libs/go2idl/client-gen/generators/fake/BUILD @@ -19,6 +19,7 @@ go_library( deps = [ "//cmd/libs/go2idl/client-gen/args:go_default_library", "//cmd/libs/go2idl/client-gen/generators/scheme:go_default_library", + "//cmd/libs/go2idl/client-gen/path:go_default_library", "//cmd/libs/go2idl/client-gen/types:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/gengo/generator:go_default_library", diff --git a/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_type.go b/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_type.go index c5c58719033..f6a4c3310b5 100644 --- a/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_type.go +++ b/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_type.go @@ -24,6 +24,7 @@ import ( "k8s.io/gengo/generator" "k8s.io/gengo/namer" "k8s.io/gengo/types" + "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/path" ) // genFakeForType produces a file for each top-level type. @@ -99,7 +100,7 @@ func (g *genFakeForType) GenerateType(c *generator.Context, t *types.Type, w io. } // allow user to define a group name that's different from the one parsed from the directory. - p := c.Universe.Package(g.inputPackage) + p := c.Universe.Package(path.Vendorless(g.inputPackage)) if override := types.ExtractCommentTags("+", p.DocComments)["groupName"]; override != nil { groupName = override[0] } diff --git a/cmd/libs/go2idl/client-gen/generators/generator_for_group.go b/cmd/libs/go2idl/client-gen/generators/generator_for_group.go index 39de23e52f6..c419ad25149 100644 --- a/cmd/libs/go2idl/client-gen/generators/generator_for_group.go +++ b/cmd/libs/go2idl/client-gen/generators/generator_for_group.go @@ -23,6 +23,7 @@ import ( "k8s.io/gengo/generator" "k8s.io/gengo/namer" "k8s.io/gengo/types" + "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/path" ) // genGroup produces a file for a group client, e.g. ExtensionsClient for the extension group. @@ -76,7 +77,7 @@ func (g *genGroup) GenerateType(c *generator.Context, t *types.Type, w io.Writer groupName = "" } // allow user to define a group name that's different from the one parsed from the directory. - p := c.Universe.Package(g.inputPackage) + p := c.Universe.Package(path.Vendorless(g.inputPackage)) if override := types.ExtractCommentTags("+", p.DocComments)["groupName"]; override != nil { groupName = override[0] } @@ -95,7 +96,7 @@ func (g *genGroup) GenerateType(c *generator.Context, t *types.Type, w io.Writer "restDefaultKubernetesUserAgent": c.Universe.Function(types.Name{Package: "k8s.io/client-go/rest", Name: "DefaultKubernetesUserAgent"}), "restRESTClientInterface": c.Universe.Type(types.Name{Package: "k8s.io/client-go/rest", Name: "Interface"}), "restRESTClientFor": c.Universe.Function(types.Name{Package: "k8s.io/client-go/rest", Name: "RESTClientFor"}), - "SchemeGroupVersion": c.Universe.Variable(types.Name{Package: g.inputPackage, Name: "SchemeGroupVersion"}), + "SchemeGroupVersion": c.Universe.Variable(types.Name{Package: path.Vendorless(g.inputPackage), Name: "SchemeGroupVersion"}), } sw.Do(groupInterfaceTemplate, m) sw.Do(groupClientTemplate, m) diff --git a/cmd/libs/go2idl/client-gen/generators/scheme/BUILD b/cmd/libs/go2idl/client-gen/generators/scheme/BUILD index de5835d84a2..d3f058386c8 100644 --- a/cmd/libs/go2idl/client-gen/generators/scheme/BUILD +++ b/cmd/libs/go2idl/client-gen/generators/scheme/BUILD @@ -12,6 +12,7 @@ go_library( srcs = ["generator_for_scheme.go"], tags = ["automanaged"], deps = [ + "//cmd/libs/go2idl/client-gen/path:go_default_library", "//cmd/libs/go2idl/client-gen/types:go_default_library", "//vendor/k8s.io/gengo/generator:go_default_library", "//vendor/k8s.io/gengo/namer:go_default_library", diff --git a/cmd/libs/go2idl/client-gen/generators/scheme/generator_for_scheme.go b/cmd/libs/go2idl/client-gen/generators/scheme/generator_for_scheme.go index 869c76a3b54..e4a2b02e14d 100644 --- a/cmd/libs/go2idl/client-gen/generators/scheme/generator_for_scheme.go +++ b/cmd/libs/go2idl/client-gen/generators/scheme/generator_for_scheme.go @@ -26,6 +26,7 @@ import ( "k8s.io/gengo/generator" "k8s.io/gengo/namer" "k8s.io/gengo/types" + "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/path" clientgentypes "k8s.io/kubernetes/cmd/libs/go2idl/client-gen/types" ) @@ -66,10 +67,10 @@ func (g *GenScheme) Imports(c *generator.Context) (imports []string) { packagePath = filepath.Dir(packagePath) } packagePath = filepath.Join(packagePath, "install") - imports = append(imports, strings.ToLower(fmt.Sprintf("%s \"%s\"", group.Group.NonEmpty(), packagePath))) + imports = append(imports, strings.ToLower(fmt.Sprintf("%s \"%s\"", group.Group.NonEmpty(), path.Vendorless(packagePath)))) break } else { - imports = append(imports, strings.ToLower(fmt.Sprintf("%s%s \"%s\"", group.Group.NonEmpty(), version.NonEmpty(), packagePath))) + imports = append(imports, strings.ToLower(fmt.Sprintf("%s%s \"%s\"", group.Group.NonEmpty(), version.NonEmpty(), path.Vendorless(packagePath)))) } } } diff --git a/cmd/libs/go2idl/client-gen/path/BUILD b/cmd/libs/go2idl/client-gen/path/BUILD new file mode 100644 index 00000000000..1c967c9b846 --- /dev/null +++ b/cmd/libs/go2idl/client-gen/path/BUILD @@ -0,0 +1,27 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["path.go"], + tags = ["automanaged"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/cmd/libs/go2idl/client-gen/path/path.go b/cmd/libs/go2idl/client-gen/path/path.go new file mode 100644 index 00000000000..19b269bdf28 --- /dev/null +++ b/cmd/libs/go2idl/client-gen/path/path.go @@ -0,0 +1,31 @@ +/* +Copyright 2017 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 path + +import "strings" + +// Vendorless removes the longest match of "*/vendor/" from the front of p. +// It is useful if a package locates in vendor/, e.g., +// k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/apis/meta/v1, because gengo +// indexes the package with its import path, e.g., +// k8s.io/apimachinery/pkg/apis/meta/v1, +func Vendorless(p string) string { + if pos := strings.LastIndex(p, "/vendor/"); pos != -1 { + return p[pos+len("/vendor/"):] + } + return p +} diff --git a/federation/cluster/federation-down.sh b/federation/cluster/federation-down.sh index f3f71b2ae26..be91e974f60 100755 --- a/federation/cluster/federation-down.sh +++ b/federation/cluster/federation-down.sh @@ -52,7 +52,8 @@ function unjoin_clusters() { "${context}" \ --federation-system-namespace=${FEDERATION_NAMESPACE} \ --context="${FEDERATION_KUBE_CONTEXT}" \ - --host-cluster-context="${HOST_CLUSTER_CONTEXT}" + --host-cluster-context="${HOST_CLUSTER_CONTEXT}" \ + --v=4 done } diff --git a/federation/cluster/federation-up.sh b/federation/cluster/federation-up.sh index b5c120353a1..62efca9cdfc 100755 --- a/federation/cluster/federation-up.sh +++ b/federation/cluster/federation-up.sh @@ -94,7 +94,8 @@ function init() { --apiserver-enable-basic-auth=true \ --apiserver-enable-token-auth=true \ --apiserver-arg-overrides="--v=4" \ - --controllermanager-arg-overrides="--v=4" + --controllermanager-arg-overrides="--v=4" \ + --v=4 } # join_clusters joins the clusters in the local kubeconfig to federation. The clusters @@ -107,7 +108,8 @@ function join_clusters() { "${context}" \ --federation-system-namespace=${FEDERATION_NAMESPACE} \ --host-cluster-context="${HOST_CLUSTER_CONTEXT}" \ - --context="${FEDERATION_KUBE_CONTEXT}" + --context="${FEDERATION_KUBE_CONTEXT}" \ + --v=4 done } diff --git a/federation/pkg/federation-controller/util/federated_updater.go b/federation/pkg/federation-controller/util/federated_updater.go index 244fa4c554b..dafb8ab3bd2 100644 --- a/federation/pkg/federation-controller/util/federated_updater.go +++ b/federation/pkg/federation-controller/util/federated_updater.go @@ -122,7 +122,7 @@ func (fu *federatedUpdaterImpl) Update(ops []FederatedOperation) error { fu.recordEvent(op.Obj, eventType, "Deleting", eventArgs...) err = fu.deleteFunction(clientset, op.Obj) // IsNotFound error is fine since that means the object is deleted already. - if err != nil && !errors.IsNotFound(err) { + if errors.IsNotFound(err) { err = nil } } diff --git a/hack/.linted_packages b/hack/.linted_packages index 81eec578ff3..df10c78f4e4 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -26,6 +26,7 @@ cmd/kubectl cmd/kubelet cmd/libs/go2idl/client-gen cmd/libs/go2idl/client-gen/generators +cmd/libs/go2idl/client-gen/path cmd/libs/go2idl/client-gen/test_apis/testgroup/install cmd/libs/go2idl/conversion-gen cmd/libs/go2idl/deepcopy-gen @@ -86,6 +87,7 @@ pkg/apis/settings/install pkg/apis/settings/validation pkg/apis/storage/install pkg/apis/storage/validation +pkg/auth/nodeidentifier pkg/bootstrap/api pkg/client/conditions pkg/client/informers/informers_generated/externalversions diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index 61fed3bfff2..1188fee113f 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -25,6 +25,7 @@ DOCKERIZE_KUBELET=${DOCKERIZE_KUBELET:-""} ALLOW_PRIVILEGED=${ALLOW_PRIVILEGED:-""} ALLOW_SECURITY_CONTEXT=${ALLOW_SECURITY_CONTEXT:-""} PSP_ADMISSION=${PSP_ADMISSION:-""} +NODE_ADMISSION=${NODE_ADMISSION:-""} RUNTIME_CONFIG=${RUNTIME_CONFIG:-""} KUBELET_AUTHORIZATION_WEBHOOK=${KUBELET_AUTHORIZATION_WEBHOOK:-""} KUBELET_AUTHENTICATION_WEBHOOK=${KUBELET_AUTHENTICATION_WEBHOOK:-""} @@ -388,6 +389,9 @@ function start_apiserver { if [[ -n "${PSP_ADMISSION}" ]]; then security_admission=",PodSecurityPolicy" fi + if [[ -n "${NODE_ADMISSION}" ]]; then + security_admission=",NodeRestriction" + fi # Admission Controllers to invoke prior to persisting objects in cluster ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},ResourceQuota,DefaultStorageClass,DefaultTolerationSeconds diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 312c992b69b..6a78067ff41 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -85,7 +85,6 @@ cgroups-per-qos chaos-chance cidr-allocator-type clean-start -cleanup cleanup-iptables client-ca-file client-certificate @@ -273,7 +272,6 @@ federation-name federation-system-namespace federation-upgrade-target file-check-frequency -file_content_in_loop file-suffix flex-volume-plugin-dir forward-services @@ -302,7 +300,6 @@ google-json-key grace-period ha-domain hairpin-mode -hard hard-pod-affinity-symmetric-weight healthz-bind-address healthz-port @@ -563,7 +560,6 @@ pv-recycler-minimum-timeout-nfs pv-recycler-pod-template-filepath-hostpath pv-recycler-pod-template-filepath-nfs pv-recycler-timeout-increment-hostpath -quiet read-only-port really-crash-for-testing reconcile-cidr @@ -592,13 +588,11 @@ requestheader-username-headers required-contexts require-kubeconfig resolv-conf -resource resource-container resource-name resource-quota-sync-period resource-version results-dir -retry_time rkt-api-endpoint rkt-path rkt-stage1-image @@ -617,7 +611,6 @@ schedule-pods-here scheduler-config scheduler-name schema-cache-dir -scopes scrape-timeout seccomp-profile-root secondary-node-eviction-rate @@ -668,7 +661,6 @@ storage-media-type storage-version storage-versions streaming-connection-idle-timeout -subresource suicide-timeout sync-frequency system-cgroups @@ -712,7 +704,6 @@ use-service-account-credentials user-whitelist use-service-account-credentials use-taint-based-evictions -verb verify-only versioned-clientset-package viper-config diff --git a/pkg/BUILD b/pkg/BUILD index 83cd2a523df..2f5f692342c 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -31,6 +31,7 @@ filegroup( "//pkg/apis/settings:all-srcs", "//pkg/apis/storage:all-srcs", "//pkg/auth/authorizer/abac:all-srcs", + "//pkg/auth/nodeidentifier:all-srcs", "//pkg/auth/user:all-srcs", "//pkg/bootstrap/api:all-srcs", "//pkg/capabilities:all-srcs", diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index ec91c03276b..45e935b90aa 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -21,11 +21,14 @@ import ( "k8s.io/kubernetes/pkg/api" ) +// Visitor is called with each object name, and returns true if visiting should continue +type Visitor func(name string) (shouldContinue bool) + // VisitPodSecretNames invokes the visitor function with the name of every secret // referenced by the pod spec. If visitor returns false, visiting is short-circuited. // Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. // Returns true if visiting completed, false if visiting was short-circuited. -func VisitPodSecretNames(pod *api.Pod, visitor func(string) bool) bool { +func VisitPodSecretNames(pod *api.Pod, visitor Visitor) bool { for _, reference := range pod.Spec.ImagePullSecrets { if !visitor(reference.Name) { return false @@ -86,7 +89,7 @@ func VisitPodSecretNames(pod *api.Pod, visitor func(string) bool) bool { return true } -func visitContainerSecretNames(container *api.Container, visitor func(string) bool) bool { +func visitContainerSecretNames(container *api.Container, visitor Visitor) bool { for _, env := range container.EnvFrom { if env.SecretRef != nil { if !visitor(env.SecretRef.Name) { @@ -104,6 +107,60 @@ func visitContainerSecretNames(container *api.Container, visitor func(string) bo return true } +// VisitPodConfigmapNames invokes the visitor function with the name of every configmap +// referenced by the pod spec. If visitor returns false, visiting is short-circuited. +// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. +// Returns true if visiting completed, false if visiting was short-circuited. +func VisitPodConfigmapNames(pod *api.Pod, visitor Visitor) bool { + for i := range pod.Spec.InitContainers { + if !visitContainerConfigmapNames(&pod.Spec.InitContainers[i], visitor) { + return false + } + } + for i := range pod.Spec.Containers { + if !visitContainerConfigmapNames(&pod.Spec.Containers[i], visitor) { + return false + } + } + var source *api.VolumeSource + for i := range pod.Spec.Volumes { + source = &pod.Spec.Volumes[i].VolumeSource + switch { + case source.Projected != nil: + for j := range source.Projected.Sources { + if source.Projected.Sources[j].ConfigMap != nil { + if !visitor(source.Projected.Sources[j].ConfigMap.Name) { + return false + } + } + } + case source.ConfigMap != nil: + if !visitor(source.ConfigMap.Name) { + return false + } + } + } + return true +} + +func visitContainerConfigmapNames(container *api.Container, visitor Visitor) bool { + for _, env := range container.EnvFrom { + if env.ConfigMapRef != nil { + if !visitor(env.ConfigMapRef.Name) { + return false + } + } + } + for _, envVar := range container.Env { + if envVar.ValueFrom != nil && envVar.ValueFrom.ConfigMapKeyRef != nil { + if !visitor(envVar.ValueFrom.ConfigMapKeyRef.Name) { + return false + } + } + } + return true +} + // IsPodReady returns true if a pod is ready; false otherwise. func IsPodReady(pod *api.Pod) bool { return IsPodReadyConditionTrue(pod.Status) diff --git a/pkg/api/v1/pod/util.go b/pkg/api/v1/pod/util.go index bb24a3c7626..85eb5338337 100644 --- a/pkg/api/v1/pod/util.go +++ b/pkg/api/v1/pod/util.go @@ -107,11 +107,14 @@ func SetInitContainersStatusesAnnotations(pod *v1.Pod) error { return nil } +// Visitor is called with each object name, and returns true if visiting should continue +type Visitor func(name string) (shouldContinue bool) + // VisitPodSecretNames invokes the visitor function with the name of every secret // referenced by the pod spec. If visitor returns false, visiting is short-circuited. // Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. // Returns true if visiting completed, false if visiting was short-circuited. -func VisitPodSecretNames(pod *v1.Pod, visitor func(string) bool) bool { +func VisitPodSecretNames(pod *v1.Pod, visitor Visitor) bool { for _, reference := range pod.Spec.ImagePullSecrets { if !visitor(reference.Name) { return false @@ -173,7 +176,7 @@ func VisitPodSecretNames(pod *v1.Pod, visitor func(string) bool) bool { return true } -func visitContainerSecretNames(container *v1.Container, visitor func(string) bool) bool { +func visitContainerSecretNames(container *v1.Container, visitor Visitor) bool { for _, env := range container.EnvFrom { if env.SecretRef != nil { if !visitor(env.SecretRef.Name) { diff --git a/pkg/auth/nodeidentifier/BUILD b/pkg/auth/nodeidentifier/BUILD new file mode 100644 index 00000000000..cc228ad79c1 --- /dev/null +++ b/pkg/auth/nodeidentifier/BUILD @@ -0,0 +1,40 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = [ + "default.go", + "interfaces.go", + ], + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["default_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/auth/nodeidentifier/default.go b/pkg/auth/nodeidentifier/default.go new file mode 100644 index 00000000000..80df38ba4f3 --- /dev/null +++ b/pkg/auth/nodeidentifier/default.go @@ -0,0 +1,64 @@ +/* +Copyright 2017 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 nodeidentifier + +import ( + "strings" + + "k8s.io/apiserver/pkg/authentication/user" +) + +// NewDefaultNodeIdentifier returns a default NodeIdentifier implementation, +// which returns isNode=true if the user groups contain the system:nodes group, +// and populates nodeName if isNode is true, and the user name is in the format system:node: +func NewDefaultNodeIdentifier() NodeIdentifier { + return defaultNodeIdentifier{} +} + +// defaultNodeIdentifier implements NodeIdentifier +type defaultNodeIdentifier struct{} + +// nodeUserNamePrefix is the prefix for usernames in the form `system:node:` +const nodeUserNamePrefix = "system:node:" + +// NodeIdentity returns isNode=true if the user groups contain the system:nodes group, +// and populates nodeName if isNode is true, and the user name is in the format system:node: +func (defaultNodeIdentifier) NodeIdentity(u user.Info) (string, bool) { + // Make sure we're a node, and can parse the node name + if u == nil { + return "", false + } + + isNode := false + for _, g := range u.GetGroups() { + if g == user.NodesGroup { + isNode = true + break + } + } + if !isNode { + return "", false + } + + userName := u.GetName() + nodeName := "" + if strings.HasPrefix(userName, nodeUserNamePrefix) { + nodeName = strings.TrimPrefix(userName, nodeUserNamePrefix) + } + + return nodeName, isNode +} diff --git a/pkg/auth/nodeidentifier/default_test.go b/pkg/auth/nodeidentifier/default_test.go new file mode 100644 index 00000000000..fee38d57296 --- /dev/null +++ b/pkg/auth/nodeidentifier/default_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2017 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 nodeidentifier + +import ( + "testing" + + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestDefaultNodeIdentifier_NodeIdentity(t *testing.T) { + tests := []struct { + name string + user user.Info + expectNodeName string + expectIsNode bool + }{ + { + name: "nil user", + user: nil, + expectNodeName: "", + expectIsNode: false, + }, + { + name: "node username without group", + user: &user.DefaultInfo{Name: "system:node:foo"}, + expectNodeName: "", + expectIsNode: false, + }, + { + name: "node group without username", + user: &user.DefaultInfo{Name: "foo", Groups: []string{"system:nodes"}}, + expectNodeName: "", + expectIsNode: true, + }, + { + name: "node group and username", + user: &user.DefaultInfo{Name: "system:node:foo", Groups: []string{"system:nodes"}}, + expectNodeName: "foo", + expectIsNode: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nodeName, isNode := NewDefaultNodeIdentifier().NodeIdentity(tt.user) + if nodeName != tt.expectNodeName { + t.Errorf("DefaultNodeIdentifier.NodeIdentity() got = %v, want %v", nodeName, tt.expectNodeName) + } + if isNode != tt.expectIsNode { + t.Errorf("DefaultNodeIdentifier.NodeIdentity() got1 = %v, want %v", isNode, tt.expectIsNode) + } + }) + } +} diff --git a/pkg/auth/nodeidentifier/interfaces.go b/pkg/auth/nodeidentifier/interfaces.go new file mode 100644 index 00000000000..917bebaf9d9 --- /dev/null +++ b/pkg/auth/nodeidentifier/interfaces.go @@ -0,0 +1,30 @@ +/* +Copyright 2017 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 nodeidentifier + +import ( + "k8s.io/apiserver/pkg/authentication/user" +) + +// NodeIdentifier determines node information from a given user +type NodeIdentifier interface { + // IdentifyNode determines node information from the given user.Info. + // nodeName is the name of the Node API object associated with the user.Info, + // and may be empty if a specific node cannot be determined. + // isNode is true if the user.Info represents an identity issued to a node. + NodeIdentity(user.Info) (nodeName string, isNode bool) +} diff --git a/pkg/cloudprovider/BUILD b/pkg/cloudprovider/BUILD index 134484b5e9c..e638a06508e 100644 --- a/pkg/cloudprovider/BUILD +++ b/pkg/cloudprovider/BUILD @@ -17,6 +17,7 @@ go_library( tags = ["automanaged"], deps = [ "//pkg/api/v1:go_default_library", + "//pkg/controller:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", ], diff --git a/pkg/cloudprovider/cloud.go b/pkg/cloudprovider/cloud.go index 2810ab3019f..a9217e21bec 100644 --- a/pkg/cloudprovider/cloud.go +++ b/pkg/cloudprovider/cloud.go @@ -23,10 +23,13 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/controller" ) // Interface is an abstract, pluggable interface for cloud providers. type Interface interface { + // Initialize provides the cloud with a kubernetes client builder + Initialize(clientBuilder controller.ControllerClientBuilder) // LoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise. LoadBalancer() (LoadBalancer, bool) // Instances returns an instances interface. Also returns true if the interface is supported, false otherwise. diff --git a/pkg/cloudprovider/providers/aws/BUILD b/pkg/cloudprovider/providers/aws/BUILD index 47e16a82c45..0a0673595cb 100644 --- a/pkg/cloudprovider/providers/aws/BUILD +++ b/pkg/cloudprovider/providers/aws/BUILD @@ -30,6 +30,7 @@ go_library( "//pkg/api/v1:go_default_library", "//pkg/api/v1/service:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//pkg/credentialprovider/aws:go_default_library", "//pkg/volume:go_default_library", "//vendor/github.com/aws/aws-sdk-go/aws:go_default_library", diff --git a/pkg/cloudprovider/providers/aws/aws.go b/pkg/cloudprovider/providers/aws/aws.go index 1667ff015a4..8d42884e146 100644 --- a/pkg/cloudprovider/providers/aws/aws.go +++ b/pkg/cloudprovider/providers/aws/aws.go @@ -49,6 +49,7 @@ import ( "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1/service" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/volume" ) @@ -888,6 +889,9 @@ func newAWSCloud(config io.Reader, awsServices Services) (*Cloud, error) { return awsCloud, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (c *Cloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // Clusters returns the list of clusters. func (c *Cloud) Clusters() (cloudprovider.Clusters, bool) { return nil, false diff --git a/pkg/cloudprovider/providers/azure/BUILD b/pkg/cloudprovider/providers/azure/BUILD index 5192a8b5e93..3c7707158c1 100644 --- a/pkg/cloudprovider/providers/azure/BUILD +++ b/pkg/cloudprovider/providers/azure/BUILD @@ -29,6 +29,7 @@ go_library( "//pkg/api/v1:go_default_library", "//pkg/api/v1/service:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//pkg/version:go_default_library", "//pkg/volume:go_default_library", "//vendor/github.com/Azure/azure-sdk-for-go/arm/compute:go_default_library", diff --git a/pkg/cloudprovider/providers/azure/azure.go b/pkg/cloudprovider/providers/azure/azure.go index cd22f7beb5e..7694693e261 100644 --- a/pkg/cloudprovider/providers/azure/azure.go +++ b/pkg/cloudprovider/providers/azure/azure.go @@ -20,8 +20,10 @@ import ( "fmt" "io" "io/ioutil" + "time" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/version" "github.com/Azure/azure-sdk-for-go/arm/compute" @@ -30,7 +32,6 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" "github.com/ghodss/yaml" - "time" ) // CloudProviderName is the value used for the --cloud-provider flag @@ -179,6 +180,9 @@ func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { return &az, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (az *Cloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // LoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise. func (az *Cloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) { return az, true diff --git a/pkg/cloudprovider/providers/cloudstack/BUILD b/pkg/cloudprovider/providers/cloudstack/BUILD index 10928e0f0ad..52428e7e25e 100644 --- a/pkg/cloudprovider/providers/cloudstack/BUILD +++ b/pkg/cloudprovider/providers/cloudstack/BUILD @@ -18,6 +18,7 @@ go_library( deps = [ "//pkg/api/v1:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/xanzy/go-cloudstack/cloudstack:go_default_library", "//vendor/gopkg.in/gcfg.v1:go_default_library", diff --git a/pkg/cloudprovider/providers/cloudstack/cloudstack.go b/pkg/cloudprovider/providers/cloudstack/cloudstack.go index 308e6a85f6d..f8e2b3ffb6f 100644 --- a/pkg/cloudprovider/providers/cloudstack/cloudstack.go +++ b/pkg/cloudprovider/providers/cloudstack/cloudstack.go @@ -24,6 +24,7 @@ import ( "github.com/xanzy/go-cloudstack/cloudstack" "gopkg.in/gcfg.v1" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) // ProviderName is the name of this cloud provider. @@ -81,6 +82,9 @@ func newCSCloud(cfg *CSConfig) (*CSCloud, error) { return &CSCloud{client, cfg.Global.ProjectID, cfg.Global.Zone}, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (cs *CSCloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // LoadBalancer returns an implementation of LoadBalancer for CloudStack. func (cs *CSCloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) { return cs, true diff --git a/pkg/cloudprovider/providers/fake/BUILD b/pkg/cloudprovider/providers/fake/BUILD index 5b586ac810a..489d7312e77 100644 --- a/pkg/cloudprovider/providers/fake/BUILD +++ b/pkg/cloudprovider/providers/fake/BUILD @@ -17,6 +17,7 @@ go_library( deps = [ "//pkg/api/v1:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", ], ) diff --git a/pkg/cloudprovider/providers/fake/fake.go b/pkg/cloudprovider/providers/fake/fake.go index d7b59ac7ffa..dcca77f5118 100644 --- a/pkg/cloudprovider/providers/fake/fake.go +++ b/pkg/cloudprovider/providers/fake/fake.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const defaultProviderName = "fake" @@ -82,6 +83,9 @@ func (f *FakeCloud) ClearCalls() { f.Calls = []string{} } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (f *FakeCloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + func (f *FakeCloud) ListClusters() ([]string, error) { return f.ClusterList, f.Err } diff --git a/pkg/cloudprovider/providers/gce/BUILD b/pkg/cloudprovider/providers/gce/BUILD index 7cdfe7c2a5d..206d0681660 100644 --- a/pkg/cloudprovider/providers/gce/BUILD +++ b/pkg/cloudprovider/providers/gce/BUILD @@ -38,6 +38,7 @@ go_library( "//pkg/api/v1:go_default_library", "//pkg/api/v1/service:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//pkg/util/net/sets:go_default_library", "//pkg/volume:go_default_library", "//vendor/cloud.google.com/go/compute/metadata:go_default_library", diff --git a/pkg/cloudprovider/providers/gce/gce.go b/pkg/cloudprovider/providers/gce/gce.go index e3b37f2b77f..96e2bd14bdf 100644 --- a/pkg/cloudprovider/providers/gce/gce.go +++ b/pkg/cloudprovider/providers/gce/gce.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/flowcontrol" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" "github.com/golang/glog" "golang.org/x/oauth2" @@ -233,6 +234,9 @@ func CreateGCECloud(projectID, region, zone string, managedZones []string, netwo }, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (gce *GCECloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // LoadBalancer returns an implementation of LoadBalancer for Google Compute Engine. func (gce *GCECloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) { return gce, true diff --git a/pkg/cloudprovider/providers/gce/gce_instances.go b/pkg/cloudprovider/providers/gce/gce_instances.go index e6616fed49d..6446f26ff18 100644 --- a/pkg/cloudprovider/providers/gce/gce_instances.go +++ b/pkg/cloudprovider/providers/gce/gce_instances.go @@ -17,7 +17,6 @@ limitations under the License. package gce import ( - "errors" "fmt" "net/http" "strconv" @@ -62,7 +61,27 @@ func (gce *GCECloud) NodeAddresses(_ types.NodeName) ([]v1.NodeAddress, error) { // This method will not be called from the node that is requesting this ID. // i.e. metadata service and other local methods cannot be used here func (gce *GCECloud) NodeAddressesByProviderID(providerID string) ([]v1.NodeAddress, error) { - return []v1.NodeAddress{}, errors.New("unimplemented") + project, zone, name, err := splitProviderID(providerID) + if err != nil { + return []v1.NodeAddress{}, err + } + + instance, err := gce.service.Instances.Get(project, zone, canonicalizeInstanceName(name)).Do() + if err != nil { + return []v1.NodeAddress{}, fmt.Errorf("error while querying for providerID %q: %v", providerID, err) + } + + if len(instance.NetworkInterfaces) < 1 { + return []v1.NodeAddress{}, fmt.Errorf("could not find network interfaces for providerID %q", providerID) + } + networkInterface := instance.NetworkInterfaces[0] + + nodeAddresses := []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: networkInterface.NetworkIP}} + for _, config := range networkInterface.AccessConfigs { + nodeAddresses = append(nodeAddresses, v1.NodeAddress{Type: v1.NodeExternalIP, Address: config.NatIP}) + } + + return nodeAddresses, nil } // InstanceTypeByProviderID returns the cloudprovider instance type of the node @@ -70,7 +89,15 @@ func (gce *GCECloud) NodeAddressesByProviderID(providerID string) ([]v1.NodeAddr // node that is requesting this ID. i.e. metadata service and other local // methods cannot be used here func (gce *GCECloud) InstanceTypeByProviderID(providerID string) (string, error) { - return "", errors.New("unimplemented") + project, zone, name, err := splitProviderID(providerID) + if err != nil { + return "", err + } + instance, err := gce.getInstanceFromProjectInZoneByName(project, zone, name) + if err != nil { + return "", err + } + return instance.Type, nil } // ExternalID returns the cloud provider ID of the node with the specified NodeName (deprecated). @@ -339,30 +366,38 @@ func (gce *GCECloud) getInstancesByNames(names []string) ([]*gceInstance, error) func (gce *GCECloud) getInstanceByName(name string) (*gceInstance, error) { // Avoid changing behaviour when not managing multiple zones for _, zone := range gce.managedZones { - name = canonicalizeInstanceName(name) - mc := newInstancesMetricContext("get", zone) - res, err := gce.service.Instances.Get(gce.projectID, zone, name).Do() - mc.Observe(err) + instance, err := gce.getInstanceFromProjectInZoneByName(gce.projectID, zone, name) if err != nil { - glog.Errorf("getInstanceByName: failed to get instance %s; err: %v", name, err) - if isHTTPErrorCode(err, http.StatusNotFound) { continue } return nil, err } - return &gceInstance{ - Zone: lastComponent(res.Zone), - Name: res.Name, - ID: res.Id, - Disks: res.Disks, - Type: lastComponent(res.MachineType), - }, nil + return instance, nil } return nil, cloudprovider.InstanceNotFound } +func (gce *GCECloud) getInstanceFromProjectInZoneByName(project, zone, name string) (*gceInstance, error) { + name = canonicalizeInstanceName(name) + mc := newInstancesMetricContext("get", zone) + res, err := gce.service.Instances.Get(project, zone, name).Do() + mc.Observe(err) + if err != nil { + glog.Errorf("getInstanceFromProjectInZoneByName: failed to get instance %s; err: %v", name, err) + return nil, err + } + + return &gceInstance{ + Zone: lastComponent(res.Zone), + Name: res.Name, + ID: res.Id, + Disks: res.Disks, + Type: lastComponent(res.MachineType), + }, nil +} + func getInstanceIDViaMetadata() (string, error) { result, err := metadata.Get("instance/hostname") if err != nil { diff --git a/pkg/cloudprovider/providers/gce/gce_test.go b/pkg/cloudprovider/providers/gce/gce_test.go index 5bba03bf97c..c05f0bdccbe 100644 --- a/pkg/cloudprovider/providers/gce/gce_test.go +++ b/pkg/cloudprovider/providers/gce/gce_test.go @@ -158,3 +158,93 @@ func TestCreateFirewallFails(t *testing.T) { t.Errorf("error expected when creating firewall without any tags found") } } + +func TestSplitProviderID(t *testing.T) { + providers := []struct { + providerID string + + project string + zone string + instance string + + fail bool + }{ + { + providerID: ProviderName + "://project-example-164317/us-central1-f/kubernetes-node-fhx1", + project: "project-example-164317", + zone: "us-central1-f", + instance: "kubernetes-node-fhx1", + fail: false, + }, + { + providerID: ProviderName + "://project-example.164317/us-central1-f/kubernetes-node-fhx1", + project: "project-example.164317", + zone: "us-central1-f", + instance: "kubernetes-node-fhx1", + fail: false, + }, + { + providerID: ProviderName + "://project-example-164317/us-central1-fkubernetes-node-fhx1", + project: "", + zone: "", + instance: "", + fail: true, + }, + { + providerID: ProviderName + ":/project-example-164317/us-central1-f/kubernetes-node-fhx1", + project: "", + zone: "", + instance: "", + fail: true, + }, + { + providerID: "aws://project-example-164317/us-central1-f/kubernetes-node-fhx1", + project: "", + zone: "", + instance: "", + fail: true, + }, + { + providerID: ProviderName + "://project-example-164317/us-central1-f/kubernetes-node-fhx1/", + project: "", + zone: "", + instance: "", + fail: true, + }, + { + providerID: ProviderName + "://project-example.164317//kubernetes-node-fhx1", + project: "", + zone: "", + instance: "", + fail: true, + }, + { + providerID: ProviderName + "://project-example.164317/kubernetes-node-fhx1", + project: "", + zone: "", + instance: "", + fail: true, + }, + } + + for _, test := range providers { + project, zone, instance, err := splitProviderID(test.providerID) + if (err != nil) != test.fail { + t.Errorf("Expected to failt=%t, with pattern %v", test.fail, test) + } + + if test.fail { + continue + } + + if project != test.project { + t.Errorf("Expected %v, but got %v", test.project, project) + } + if zone != test.zone { + t.Errorf("Expected %v, but got %v", test.zone, zone) + } + if instance != test.instance { + t.Errorf("Expected %v, but got %v", test.instance, instance) + } + } +} diff --git a/pkg/cloudprovider/providers/gce/gce_util.go b/pkg/cloudprovider/providers/gce/gce_util.go index 2748d7c0dd5..8f59cd5c495 100644 --- a/pkg/cloudprovider/providers/gce/gce_util.go +++ b/pkg/cloudprovider/providers/gce/gce_util.go @@ -17,7 +17,9 @@ limitations under the License. package gce import ( + "errors" "fmt" + "regexp" "strings" "k8s.io/apimachinery/pkg/types" @@ -35,6 +37,8 @@ type gceInstance struct { Type string } +var providerIdRE = regexp.MustCompile(`^` + ProviderName + `://([^/]+)/([^/]+)/([^/]+)$`) + func getProjectAndZone() (string, string, error) { result, err := metadata.Get("instance/zone") if err != nil { @@ -100,3 +104,14 @@ func isHTTPErrorCode(err error, code int) bool { apiErr, ok := err.(*googleapi.Error) return ok && apiErr.Code == code } + +// splitProviderID splits a provider's id into core components. +// A providerID is build out of '${ProviderName}://${project-id}/${zone}/${instance-name}' +// See cloudprovider.GetInstanceProviderID. +func splitProviderID(providerID string) (project, zone, instance string, err error) { + matches := providerIdRE.FindStringSubmatch(providerID) + if len(matches) != 4 { + return "", "", "", errors.New("error splitting providerID") + } + return matches[1], matches[2], matches[3], nil +} diff --git a/pkg/cloudprovider/providers/mesos/BUILD b/pkg/cloudprovider/providers/mesos/BUILD index 8b4b9d7f52d..97344163654 100644 --- a/pkg/cloudprovider/providers/mesos/BUILD +++ b/pkg/cloudprovider/providers/mesos/BUILD @@ -20,6 +20,7 @@ go_library( deps = [ "//pkg/api/v1:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/mesos/mesos-go/detector:go_default_library", "//vendor/github.com/mesos/mesos-go/detector/zoo:go_default_library", diff --git a/pkg/cloudprovider/providers/mesos/mesos.go b/pkg/cloudprovider/providers/mesos/mesos.go index 76d80e36c03..9462ff0a050 100644 --- a/pkg/cloudprovider/providers/mesos/mesos.go +++ b/pkg/cloudprovider/providers/mesos/mesos.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const ( @@ -89,6 +90,9 @@ func newMesosCloud(configReader io.Reader) (*MesosCloud, error) { } } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (c *MesosCloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // Implementation of Instances.CurrentNodeName func (c *MesosCloud) CurrentNodeName(hostname string) (types.NodeName, error) { return types.NodeName(hostname), nil diff --git a/pkg/cloudprovider/providers/openstack/BUILD b/pkg/cloudprovider/providers/openstack/BUILD index 53f86b4ee89..e10cb26aa08 100644 --- a/pkg/cloudprovider/providers/openstack/BUILD +++ b/pkg/cloudprovider/providers/openstack/BUILD @@ -24,6 +24,7 @@ go_library( "//pkg/api/v1/helper:go_default_library", "//pkg/api/v1/service:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//pkg/util/exec:go_default_library", "//pkg/util/mount:go_default_library", "//pkg/volume:go_default_library", diff --git a/pkg/cloudprovider/providers/openstack/openstack.go b/pkg/cloudprovider/providers/openstack/openstack.go index 4ad0bfd772e..26ce40dcb52 100644 --- a/pkg/cloudprovider/providers/openstack/openstack.go +++ b/pkg/cloudprovider/providers/openstack/openstack.go @@ -45,6 +45,7 @@ import ( "k8s.io/kubernetes/pkg/api/v1" v1helper "k8s.io/kubernetes/pkg/api/v1/helper" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const ProviderName = "openstack" @@ -265,6 +266,9 @@ func newOpenStack(cfg Config) (*OpenStack, error) { return &os, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (os *OpenStack) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // mapNodeNameToServerName maps a k8s NodeName to an OpenStack Server Name // This is a simple string cast. func mapNodeNameToServerName(nodeName types.NodeName) string { diff --git a/pkg/cloudprovider/providers/ovirt/BUILD b/pkg/cloudprovider/providers/ovirt/BUILD index fcbc3e9f4d6..ce90c4e1edf 100644 --- a/pkg/cloudprovider/providers/ovirt/BUILD +++ b/pkg/cloudprovider/providers/ovirt/BUILD @@ -15,6 +15,7 @@ go_library( deps = [ "//pkg/api/v1:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/gopkg.in/gcfg.v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", ], diff --git a/pkg/cloudprovider/providers/ovirt/ovirt.go b/pkg/cloudprovider/providers/ovirt/ovirt.go index b0257cbc83c..8273c0b170e 100644 --- a/pkg/cloudprovider/providers/ovirt/ovirt.go +++ b/pkg/cloudprovider/providers/ovirt/ovirt.go @@ -34,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const ProviderName = "ovirt" @@ -116,7 +117,10 @@ func newOVirtCloud(config io.Reader) (*OVirtCloud, error) { return &OVirtCloud{VmsRequest: request}, nil } -func (aws *OVirtCloud) Clusters() (cloudprovider.Clusters, bool) { +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (v *OVirtCloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + +func (v *OVirtCloud) Clusters() (cloudprovider.Clusters, bool) { return nil, false } diff --git a/pkg/cloudprovider/providers/photon/BUILD b/pkg/cloudprovider/providers/photon/BUILD index d367e181b44..85c17eb901d 100644 --- a/pkg/cloudprovider/providers/photon/BUILD +++ b/pkg/cloudprovider/providers/photon/BUILD @@ -16,6 +16,7 @@ go_library( "//pkg/api/v1:go_default_library", "//pkg/api/v1/helper:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/vmware/photon-controller-go-sdk/photon:go_default_library", "//vendor/gopkg.in/gcfg.v1:go_default_library", diff --git a/pkg/cloudprovider/providers/photon/photon.go b/pkg/cloudprovider/providers/photon/photon.go index c2bb0265361..f33e9c27a89 100644 --- a/pkg/cloudprovider/providers/photon/photon.go +++ b/pkg/cloudprovider/providers/photon/photon.go @@ -41,6 +41,7 @@ import ( "k8s.io/kubernetes/pkg/api/v1" v1helper "k8s.io/kubernetes/pkg/api/v1/helper" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const ( @@ -292,6 +293,9 @@ func newPCCloud(cfg PCConfig) (*PCCloud, error) { return &pc, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (pc *PCCloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // Instances returns an implementation of Instances for Photon Controller. func (pc *PCCloud) Instances() (cloudprovider.Instances, bool) { return pc, true diff --git a/pkg/cloudprovider/providers/rackspace/BUILD b/pkg/cloudprovider/providers/rackspace/BUILD index dceda3955c7..e9c9aae426e 100644 --- a/pkg/cloudprovider/providers/rackspace/BUILD +++ b/pkg/cloudprovider/providers/rackspace/BUILD @@ -15,6 +15,7 @@ go_library( deps = [ "//pkg/api/v1:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/rackspace/gophercloud:go_default_library", "//vendor/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach:go_default_library", diff --git a/pkg/cloudprovider/providers/rackspace/rackspace.go b/pkg/cloudprovider/providers/rackspace/rackspace.go index 253b0508870..c333b2f41c5 100644 --- a/pkg/cloudprovider/providers/rackspace/rackspace.go +++ b/pkg/cloudprovider/providers/rackspace/rackspace.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const ( @@ -218,6 +219,9 @@ func newRackspace(cfg Config) (*Rackspace, error) { return &os, nil } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (os *Rackspace) Initialize(clientBuilder controller.ControllerClientBuilder) {} + type Instances struct { compute *gophercloud.ServiceClient } diff --git a/pkg/cloudprovider/providers/vsphere/BUILD b/pkg/cloudprovider/providers/vsphere/BUILD index fd29342072e..8ff6d52ce42 100644 --- a/pkg/cloudprovider/providers/vsphere/BUILD +++ b/pkg/cloudprovider/providers/vsphere/BUILD @@ -19,6 +19,7 @@ go_library( "//pkg/api/v1:go_default_library", "//pkg/api/v1/helper:go_default_library", "//pkg/cloudprovider:go_default_library", + "//pkg/controller:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/vmware/govmomi:go_default_library", "//vendor/github.com/vmware/govmomi/find:go_default_library", diff --git a/pkg/cloudprovider/providers/vsphere/vsphere.go b/pkg/cloudprovider/providers/vsphere/vsphere.go index 5d2f74e134f..6a925da18e8 100644 --- a/pkg/cloudprovider/providers/vsphere/vsphere.go +++ b/pkg/cloudprovider/providers/vsphere/vsphere.go @@ -50,6 +50,7 @@ import ( "k8s.io/kubernetes/pkg/api/v1" v1helper "k8s.io/kubernetes/pkg/api/v1/helper" "k8s.io/kubernetes/pkg/cloudprovider" + "k8s.io/kubernetes/pkg/controller" ) const ( @@ -229,6 +230,9 @@ func init() { }) } +// Initialize passes a Kubernetes clientBuilder interface to the cloud provider +func (vs *VSphere) Initialize(clientBuilder controller.ControllerClientBuilder) {} + // UUID gets the BIOS UUID via the sys interface. This UUID is known by vsphere func getvmUUID() (string, error) { id, err := ioutil.ReadFile(UUIDPath) diff --git a/pkg/controller/endpoint/endpoints_controller.go b/pkg/controller/endpoint/endpoints_controller.go index d24bc6130d6..df5db13e6f2 100644 --- a/pkg/controller/endpoint/endpoints_controller.go +++ b/pkg/controller/endpoint/endpoints_controller.go @@ -146,7 +146,6 @@ func (e *EndpointController) Run(workers int, stopCh <-chan struct{}) { go func() { defer utilruntime.HandleCrash() - time.Sleep(5 * time.Minute) // give time for our cache to fill e.checkLeftoverEndpoints() }() diff --git a/pkg/kubelet/envvars/envvars.go b/pkg/kubelet/envvars/envvars.go index 0adceb5be41..b86b444e8e8 100644 --- a/pkg/kubelet/envvars/envvars.go +++ b/pkg/kubelet/envvars/envvars.go @@ -18,6 +18,7 @@ package envvars import ( "fmt" + "net" "strconv" "strings" @@ -78,18 +79,21 @@ func makeLinkVariables(service *v1.Service) []v1.EnvVar { if sp.Protocol != "" { protocol = string(sp.Protocol) } + + hostPort := net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(int(sp.Port))) + if i == 0 { // Docker special-cases the first port. all = append(all, v1.EnvVar{ Name: prefix + "_PORT", - Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.ClusterIP, sp.Port), + Value: fmt.Sprintf("%s://%s", strings.ToLower(protocol), hostPort), }) } portPrefix := fmt.Sprintf("%s_PORT_%d_%s", prefix, sp.Port, strings.ToUpper(protocol)) all = append(all, []v1.EnvVar{ { Name: portPrefix, - Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.ClusterIP, sp.Port), + Value: fmt.Sprintf("%s://%s", strings.ToLower(protocol), hostPort), }, { Name: portPrefix + "_PROTO", diff --git a/pkg/kubelet/envvars/envvars_test.go b/pkg/kubelet/envvars/envvars_test.go index 8b192526e2b..a4ce04340ec 100644 --- a/pkg/kubelet/envvars/envvars_test.go +++ b/pkg/kubelet/envvars/envvars_test.go @@ -79,6 +79,17 @@ func TestFromServices(t *testing.T) { }, }, }, + { + ObjectMeta: metav1.ObjectMeta{Name: "super-ipv6"}, + Spec: v1.ServiceSpec{ + Selector: map[string]string{"bar": "baz"}, + ClusterIP: "2001:DB8::", + Ports: []v1.ServicePort{ + {Name: "u-d-p", Port: 8084, Protocol: "UDP"}, + {Name: "t-c-p", Port: 8084, Protocol: "TCP"}, + }, + }, + }, } vars := envvars.FromServices(sl) expected := []v1.EnvVar{ @@ -114,6 +125,19 @@ func TestFromServices(t *testing.T) { {Name: "Q_U_U_X_PORT_8083_TCP_PROTO", Value: "tcp"}, {Name: "Q_U_U_X_PORT_8083_TCP_PORT", Value: "8083"}, {Name: "Q_U_U_X_PORT_8083_TCP_ADDR", Value: "9.8.7.6"}, + {Name: "SUPER_IPV6_SERVICE_HOST", Value: "2001:DB8::"}, + {Name: "SUPER_IPV6_SERVICE_PORT", Value: "8084"}, + {Name: "SUPER_IPV6_SERVICE_PORT_U_D_P", Value: "8084"}, + {Name: "SUPER_IPV6_SERVICE_PORT_T_C_P", Value: "8084"}, + {Name: "SUPER_IPV6_PORT", Value: "udp://[2001:DB8::]:8084"}, + {Name: "SUPER_IPV6_PORT_8084_UDP", Value: "udp://[2001:DB8::]:8084"}, + {Name: "SUPER_IPV6_PORT_8084_UDP_PROTO", Value: "udp"}, + {Name: "SUPER_IPV6_PORT_8084_UDP_PORT", Value: "8084"}, + {Name: "SUPER_IPV6_PORT_8084_UDP_ADDR", Value: "2001:DB8::"}, + {Name: "SUPER_IPV6_PORT_8084_TCP", Value: "tcp://[2001:DB8::]:8084"}, + {Name: "SUPER_IPV6_PORT_8084_TCP_PROTO", Value: "tcp"}, + {Name: "SUPER_IPV6_PORT_8084_TCP_PORT", Value: "8084"}, + {Name: "SUPER_IPV6_PORT_8084_TCP_ADDR", Value: "2001:DB8::"}, } if len(vars) != len(expected) { t.Errorf("Expected %d env vars, got: %+v", len(expected), vars) diff --git a/pkg/util/bandwidth/linux.go b/pkg/util/bandwidth/linux.go index 6413930af39..9949aa3d179 100644 --- a/pkg/util/bandwidth/linux.go +++ b/pkg/util/bandwidth/linux.go @@ -101,7 +101,7 @@ func hexCIDR(cidr string) (string, error) { return "", err } ip = ip.Mask(ipnet.Mask) - hexIP := hex.EncodeToString([]byte(ip.To4())) + hexIP := hex.EncodeToString([]byte(ip)) hexMask := ipnet.Mask.String() return hexIP + "/" + hexMask, nil } @@ -119,6 +119,9 @@ func asciiCIDR(cidr string) (string, error) { ip := net.IP(ipData) maskData, err := hex.DecodeString(parts[1]) + if err != nil { + return "", err + } mask := net.IPMask(maskData) size, _ := mask.Size() diff --git a/pkg/util/bandwidth/linux_test.go b/pkg/util/bandwidth/linux_test.go index e005d65427e..980f8f845c7 100644 --- a/pkg/util/bandwidth/linux_test.go +++ b/pkg/util/bandwidth/linux_test.go @@ -94,19 +94,33 @@ func TestNextClassID(t *testing.T) { func TestHexCIDR(t *testing.T) { tests := []struct { + name string input string output string expectErr bool }{ { - input: "1.2.0.0/16", + name: "IPv4 masked", + input: "1.2.3.4/16", output: "01020000/ffff0000", }, { + name: "IPv4 host", input: "172.17.0.2/32", output: "ac110002/ffffffff", }, { + name: "IPv6 masked", + input: "2001:dead:beef::cafe/64", + output: "2001deadbeef00000000000000000000/ffffffffffffffff0000000000000000", + }, + { + name: "IPv6 host", + input: "2001::5/128", + output: "20010000000000000000000000000005/ffffffffffffffffffffffffffffffff", + }, + { + name: "invalid CIDR", input: "foo", expectErr: true, }, @@ -115,21 +129,76 @@ func TestHexCIDR(t *testing.T) { output, err := hexCIDR(test.input) if test.expectErr { if err == nil { - t.Error("unexpected non-error") + t.Errorf("case %s: unexpected non-error", test.name) } } else { if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("case %s: unexpected error: %v", test.name, err) } if output != test.output { - t.Errorf("expected: %s, saw: %s", test.output, output) + t.Errorf("case %s: expected: %s, saw: %s", + test.name, test.output, output) } - input, err := asciiCIDR(output) + } + } +} + +func TestAsciiCIDR(t *testing.T) { + tests := []struct { + name string + input string + output string + expectErr bool + }{ + { + name: "IPv4", + input: "01020000/ffff0000", + output: "1.2.0.0/16", + }, + { + name: "IPv4 host", + input: "ac110002/ffffffff", + output: "172.17.0.2/32", + }, + { + name: "IPv6", + input: "2001deadbeef00000000000000000000/ffffffffffffffff0000000000000000", + output: "2001:dead:beef::/64", + }, + { + name: "IPv6 host", + input: "20010000000000000000000000000005/ffffffffffffffffffffffffffffffff", + output: "2001::5/128", + }, + { + name: "invalid CIDR", + input: "malformed", + expectErr: true, + }, + { + name: "non-hex IP", + input: "nonhex/32", + expectErr: true, + }, + { + name: "non-hex mask", + input: "01020000/badmask", + expectErr: true, + }, + } + for _, test := range tests { + output, err := asciiCIDR(test.input) + if test.expectErr { + if err == nil { + t.Errorf("case %s: unexpected non-error", test.name) + } + } else { if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("case %s: unexpected error: %v", test.name, err) } - if input != test.input { - t.Errorf("expected: %s, saw: %s", test.input, input) + if output != test.output { + t.Errorf("case %s: expected: %s, saw: %s", + test.name, test.output, output) } } } diff --git a/plugin/BUILD b/plugin/BUILD index cf8c9c5be76..9f7368ddf2b 100644 --- a/plugin/BUILD +++ b/plugin/BUILD @@ -27,6 +27,7 @@ filegroup( "//plugin/pkg/admission/namespace/autoprovision:all-srcs", "//plugin/pkg/admission/namespace/exists:all-srcs", "//plugin/pkg/admission/namespace/lifecycle:all-srcs", + "//plugin/pkg/admission/noderestriction:all-srcs", "//plugin/pkg/admission/persistentvolume/label:all-srcs", "//plugin/pkg/admission/podnodeselector:all-srcs", "//plugin/pkg/admission/podpreset:all-srcs", diff --git a/plugin/pkg/admission/admit/admission.go b/plugin/pkg/admission/admit/admission.go index 2fce351fd45..7899e2d32fa 100644 --- a/plugin/pkg/admission/admit/admission.go +++ b/plugin/pkg/admission/admit/admission.go @@ -24,7 +24,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("AlwaysAdmit", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("AlwaysAdmit", func(config io.Reader) (admission.Interface, error) { return NewAlwaysAdmit(), nil }) } diff --git a/plugin/pkg/admission/alwayspullimages/admission.go b/plugin/pkg/admission/alwayspullimages/admission.go index 437d71ea2ba..8b0172eaa28 100644 --- a/plugin/pkg/admission/alwayspullimages/admission.go +++ b/plugin/pkg/admission/alwayspullimages/admission.go @@ -34,7 +34,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("AlwaysPullImages", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("AlwaysPullImages", func(config io.Reader) (admission.Interface, error) { return NewAlwaysPullImages(), nil }) } diff --git a/plugin/pkg/admission/antiaffinity/admission.go b/plugin/pkg/admission/antiaffinity/admission.go index 4ec64ef15fb..8aaf89a307f 100644 --- a/plugin/pkg/admission/antiaffinity/admission.go +++ b/plugin/pkg/admission/antiaffinity/admission.go @@ -28,7 +28,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("LimitPodHardAntiAffinityTopology", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("LimitPodHardAntiAffinityTopology", func(config io.Reader) (admission.Interface, error) { return NewInterPodAntiAffinity(), nil }) } diff --git a/plugin/pkg/admission/defaulttolerationseconds/admission.go b/plugin/pkg/admission/defaulttolerationseconds/admission.go index a0c9c16e2d1..cbd0c650efc 100644 --- a/plugin/pkg/admission/defaulttolerationseconds/admission.go +++ b/plugin/pkg/admission/defaulttolerationseconds/admission.go @@ -40,7 +40,12 @@ var ( ) func init() { - kubeapiserveradmission.Plugins.Register("DefaultTolerationSeconds", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("DefaultTolerationSeconds", func(config io.Reader) (admission.Interface, error) { return NewDefaultTolerationSeconds(), nil }) } diff --git a/plugin/pkg/admission/deny/admission.go b/plugin/pkg/admission/deny/admission.go index df8c0409116..0c127cfb6f8 100644 --- a/plugin/pkg/admission/deny/admission.go +++ b/plugin/pkg/admission/deny/admission.go @@ -25,7 +25,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("AlwaysDeny", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("AlwaysDeny", func(config io.Reader) (admission.Interface, error) { return NewAlwaysDeny(), nil }) } diff --git a/plugin/pkg/admission/exec/admission.go b/plugin/pkg/admission/exec/admission.go index dcdb673b539..bcf7f1dfe1b 100644 --- a/plugin/pkg/admission/exec/admission.go +++ b/plugin/pkg/admission/exec/admission.go @@ -30,13 +30,18 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("DenyEscalatingExec", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("DenyEscalatingExec", func(config io.Reader) (admission.Interface, error) { return NewDenyEscalatingExec(), nil }) // This is for legacy support of the DenyExecOnPrivileged admission controller. Most // of the time DenyEscalatingExec should be preferred. - kubeapiserveradmission.Plugins.Register("DenyExecOnPrivileged", func(config io.Reader) (admission.Interface, error) { + plugins.Register("DenyExecOnPrivileged", func(config io.Reader) (admission.Interface, error) { return NewDenyExecOnPrivileged(), nil }) } diff --git a/plugin/pkg/admission/gc/gc_admission.go b/plugin/pkg/admission/gc/gc_admission.go index 9450eb4fe9c..1f5df17dec9 100644 --- a/plugin/pkg/admission/gc/gc_admission.go +++ b/plugin/pkg/admission/gc/gc_admission.go @@ -32,7 +32,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("OwnerReferencesPermissionEnforcement", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("OwnerReferencesPermissionEnforcement", func(config io.Reader) (admission.Interface, error) { // the pods/status endpoint is ignored by this plugin since old kubelets // corrupt them. the pod status strategy ensures status updates cannot mutate // ownerRef. diff --git a/plugin/pkg/admission/imagepolicy/admission.go b/plugin/pkg/admission/imagepolicy/admission.go index cc3819b7663..1edfe576d80 100644 --- a/plugin/pkg/admission/imagepolicy/admission.go +++ b/plugin/pkg/admission/imagepolicy/admission.go @@ -50,7 +50,12 @@ var ( ) func init() { - kubeapiserveradmission.Plugins.Register("ImagePolicyWebhook", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("ImagePolicyWebhook", func(config io.Reader) (admission.Interface, error) { newImagePolicyWebhook, err := NewImagePolicyWebhook(config) if err != nil { return nil, err diff --git a/plugin/pkg/admission/initialresources/admission.go b/plugin/pkg/admission/initialresources/admission.go index 5acef40f4b8..401bb6d20f2 100644 --- a/plugin/pkg/admission/initialresources/admission.go +++ b/plugin/pkg/admission/initialresources/admission.go @@ -47,7 +47,12 @@ const ( // WARNING: this feature is experimental and will definitely change. func init() { - kubeapiserveradmission.Plugins.Register("InitialResources", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("InitialResources", func(config io.Reader) (admission.Interface, error) { // TODO: remove the usage of flags in favor of reading versioned configuration s, err := newDataSource(*source) if err != nil { diff --git a/plugin/pkg/admission/limitranger/admission.go b/plugin/pkg/admission/limitranger/admission.go index 7fcf5ea2aa5..4d16388792b 100644 --- a/plugin/pkg/admission/limitranger/admission.go +++ b/plugin/pkg/admission/limitranger/admission.go @@ -44,7 +44,12 @@ const ( ) func init() { - kubeapiserveradmission.Plugins.Register("LimitRanger", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("LimitRanger", func(config io.Reader) (admission.Interface, error) { return NewLimitRanger(&DefaultLimitRangerActions{}) }) } diff --git a/plugin/pkg/admission/namespace/autoprovision/admission.go b/plugin/pkg/admission/namespace/autoprovision/admission.go index d4054f42ac2..1de5ef6f070 100644 --- a/plugin/pkg/admission/namespace/autoprovision/admission.go +++ b/plugin/pkg/admission/namespace/autoprovision/admission.go @@ -31,7 +31,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("NamespaceAutoProvision", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("NamespaceAutoProvision", func(config io.Reader) (admission.Interface, error) { return NewProvision(), nil }) } diff --git a/plugin/pkg/admission/namespace/exists/admission.go b/plugin/pkg/admission/namespace/exists/admission.go index 6b8e348a3a1..81ae1fad32c 100644 --- a/plugin/pkg/admission/namespace/exists/admission.go +++ b/plugin/pkg/admission/namespace/exists/admission.go @@ -31,7 +31,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("NamespaceExists", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("NamespaceExists", func(config io.Reader) (admission.Interface, error) { return NewExists(), nil }) } diff --git a/plugin/pkg/admission/namespace/lifecycle/admission.go b/plugin/pkg/admission/namespace/lifecycle/admission.go index bb147584476..65ab0ef1cb0 100644 --- a/plugin/pkg/admission/namespace/lifecycle/admission.go +++ b/plugin/pkg/admission/namespace/lifecycle/admission.go @@ -51,7 +51,12 @@ const ( ) func init() { - kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic)) }) } diff --git a/plugin/pkg/admission/noderestriction/BUILD b/plugin/pkg/admission/noderestriction/BUILD new file mode 100644 index 00000000000..83fcdd19b9f --- /dev/null +++ b/plugin/pkg/admission/noderestriction/BUILD @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = ["admission.go"], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/api/pod:go_default_library", + "//pkg/auth/nodeidentifier:go_default_library", + "//pkg/client/clientset_generated/internalclientset:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", + "//pkg/kubeapiserver/admission:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["admission_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/auth/nodeidentifier:go_default_library", + "//pkg/client/clientset_generated/internalclientset/fake:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/plugin/pkg/admission/noderestriction/OWNERS b/plugin/pkg/admission/noderestriction/OWNERS new file mode 100644 index 00000000000..e58cadf54d4 --- /dev/null +++ b/plugin/pkg/admission/noderestriction/OWNERS @@ -0,0 +1,8 @@ +approvers: +- deads2k +- liggitt +- timstclair +reviewers: +- deads2k +- liggitt +- timstclair diff --git a/plugin/pkg/admission/noderestriction/admission.go b/plugin/pkg/admission/noderestriction/admission.go new file mode 100644 index 00000000000..7eabfdf7a73 --- /dev/null +++ b/plugin/pkg/admission/noderestriction/admission.go @@ -0,0 +1,203 @@ +/* +Copyright 2017 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 node + +import ( + "fmt" + "io" + + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/kubernetes/pkg/api" + podutil "k8s.io/kubernetes/pkg/api/pod" + "k8s.io/kubernetes/pkg/auth/nodeidentifier" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" +) + +const ( + PluginName = "NodeRestriction" +) + +func init() { + kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + return NewPlugin(nodeidentifier.NewDefaultNodeIdentifier(), false), nil + }) +} + +// NewPlugin creates a new NodeRestriction admission plugin. +// This plugin identifies requests from nodes +func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier, strict bool) *nodePlugin { + return &nodePlugin{ + Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), + nodeIdentifier: nodeIdentifier, + strict: strict, + } +} + +// nodePlugin holds state for and implements the admission plugin. +type nodePlugin struct { + *admission.Handler + strict bool + nodeIdentifier nodeidentifier.NodeIdentifier + podsGetter coreinternalversion.PodsGetter +} + +var ( + _ = admission.Interface(&nodePlugin{}) + _ = kubeapiserveradmission.WantsInternalKubeClientSet(&nodePlugin{}) +) + +func (p *nodePlugin) SetInternalKubeClientSet(f internalclientset.Interface) { + p.podsGetter = f.Core() +} + +func (p *nodePlugin) Validate() error { + if p.nodeIdentifier == nil { + return fmt.Errorf("%s requires a node identifier", PluginName) + } + if p.podsGetter == nil { + return fmt.Errorf("%s requires a pod getter", PluginName) + } + return nil +} + +var ( + podResource = api.Resource("pods") + nodeResource = api.Resource("nodes") +) + +func (c *nodePlugin) Admit(a admission.Attributes) error { + nodeName, isNode := c.nodeIdentifier.NodeIdentity(a.GetUserInfo()) + + // Our job is just to restrict nodes + if !isNode { + return nil + } + + if len(nodeName) == 0 { + if c.strict { + // In strict mode, disallow requests from nodes we cannot match to a particular node + return admission.NewForbidden(a, fmt.Errorf("could not determine node identity from user")) + } + // Our job is just to restrict identifiable nodes + return nil + } + + switch a.GetResource().GroupResource() { + case podResource: + switch a.GetSubresource() { + case "": + return c.admitPod(nodeName, a) + case "status": + return c.admitPodStatus(nodeName, a) + default: + return admission.NewForbidden(a, fmt.Errorf("unexpected pod subresource %s", a.GetSubresource())) + } + + case nodeResource: + return c.admitNode(nodeName, a) + + default: + return nil + } +} + +func (c *nodePlugin) admitPod(nodeName string, a admission.Attributes) error { + switch a.GetOperation() { + case admission.Create: + // require a pod object + pod, ok := a.GetObject().(*api.Pod) + if !ok { + return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject())) + } + + // only allow nodes to create mirror pods + if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; !isMirrorPod { + return admission.NewForbidden(a, fmt.Errorf("pod does not have %q annotation, node %s can only create mirror pods", api.MirrorPodAnnotationKey, nodeName)) + } + + // only allow nodes to create a pod bound to itself + if pod.Spec.NodeName != nodeName { + return admission.NewForbidden(a, fmt.Errorf("node %s can only create pods with spec.nodeName set to itself", nodeName)) + } + + // don't allow a node to create a pod that references any other API objects + if pod.Spec.ServiceAccountName != "" { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference a service account", nodeName)) + } + hasSecrets := false + podutil.VisitPodSecretNames(pod, func(name string) (shouldContinue bool) { hasSecrets = true; return false }) + if hasSecrets { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference secrets", nodeName)) + } + hasConfigMaps := false + podutil.VisitPodConfigmapNames(pod, func(name string) (shouldContinue bool) { hasConfigMaps = true; return false }) + if hasConfigMaps { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference configmaps", nodeName)) + } + for _, v := range pod.Spec.Volumes { + if v.PersistentVolumeClaim != nil { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference persistentvolumeclaims", nodeName)) + } + } + + return nil + + case admission.Delete: + // get the existing pod + existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{ResourceVersion: "0"}) + if err != nil { + return admission.NewForbidden(a, err) + } + // only allow a node to delete a pod bound to itself + if existingPod.Spec.NodeName != nodeName { + return admission.NewForbidden(a, fmt.Errorf("node %s can only delete pods with spec.nodeName set to itself", nodeName)) + } + return nil + + default: + return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation())) + } +} + +func (c *nodePlugin) admitPodStatus(nodeName string, a admission.Attributes) error { + switch a.GetOperation() { + case admission.Update: + // require an existing pod + pod, ok := a.GetOldObject().(*api.Pod) + if !ok { + return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject())) + } + // only allow a node to update status of a pod bound to itself + if pod.Spec.NodeName != nodeName { + return admission.NewForbidden(a, fmt.Errorf("node %s can only update pod status for pods with spec.nodeName set to itself", nodeName)) + } + return nil + + default: + return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation())) + } +} + +func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error { + if a.GetName() != nodeName { + return admission.NewForbidden(a, fmt.Errorf("cannot modify other nodes")) + } + return nil +} diff --git a/plugin/pkg/admission/noderestriction/admission_test.go b/plugin/pkg/admission/noderestriction/admission_test.go new file mode 100644 index 00000000000..5cb0eb23bb8 --- /dev/null +++ b/plugin/pkg/admission/noderestriction/admission_test.go @@ -0,0 +1,476 @@ +/* +Copyright 2017 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 node + +import ( + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/auth/nodeidentifier" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" +) + +func makeTestPod(namespace, name, node string, mirror bool) *api.Pod { + pod := &api.Pod{} + pod.Namespace = namespace + pod.Name = name + pod.Spec.NodeName = node + if mirror { + pod.Annotations = map[string]string{api.MirrorPodAnnotationKey: "true"} + } + return pod +} + +func Test_nodePlugin_Admit(t *testing.T) { + var ( + mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}} + bob = &user.DefaultInfo{Name: "bob"} + + mynodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "mynode"}} + othernodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "othernode"}} + + mymirrorpod = makeTestPod("ns", "mymirrorpod", "mynode", true) + othermirrorpod = makeTestPod("ns", "othermirrorpod", "othernode", true) + unboundmirrorpod = makeTestPod("ns", "unboundmirrorpod", "", true) + mypod = makeTestPod("ns", "mypod", "mynode", false) + otherpod = makeTestPod("ns", "otherpod", "othernode", false) + unboundpod = makeTestPod("ns", "unboundpod", "", false) + + configmapResource = api.Resource("configmap").WithVersion("v1") + configmapKind = api.Kind("ConfigMap").WithVersion("v1") + + podResource = api.Resource("pods").WithVersion("v1") + podKind = api.Kind("Pod").WithVersion("v1") + + nodeResource = api.Resource("nodes").WithVersion("v1") + nodeKind = api.Kind("Node").WithVersion("v1") + + noExistingPods = fake.NewSimpleClientset().Core() + existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core() + ) + + sapod := makeTestPod("ns", "mysapod", "mynode", true) + sapod.Spec.ServiceAccountName = "foo" + + secretpod := makeTestPod("ns", "mysecretpod", "mynode", true) + secretpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}}} + + configmappod := makeTestPod("ns", "myconfigmappod", "mynode", true) + configmappod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "foo"}}}}} + + pvcpod := makeTestPod("ns", "mypvcpod", "mynode", true) + pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}} + + tests := []struct { + name string + strict bool + podsGetter coreinternalversion.PodsGetter + attributes admission.Attributes + err string + }{ + // Mirror pods bound to us + { + name: "allow creating a mirror pod bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Create, mynode), + err: "", + }, + { + name: "forbid update of mirror pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow delete of mirror pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Delete, mynode), + err: "", + }, + { + name: "forbid create of mirror pod status bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow update of mirror pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Update, mynode), + err: "", + }, + { + name: "forbid delete of mirror pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Mirror pods bound to another node + { + name: "forbid creating a mirror pod bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Create, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid update of mirror pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of mirror pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of mirror pod status bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of mirror pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of mirror pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Mirror pods not bound to any node + { + name: "forbid creating a mirror pod unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Create, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid update of mirror pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of mirror pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of mirror pod status unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of mirror pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of mirror pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Normal pods bound to us + { + name: "forbid creating a normal pod bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Create, mynode), + err: "can only create mirror pods", + }, + { + name: "forbid update of normal pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow delete of normal pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Delete, mynode), + err: "", + }, + { + name: "forbid create of normal pod status bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow update of normal pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Update, mynode), + err: "", + }, + { + name: "forbid delete of normal pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Normal pods bound to another + { + name: "forbid creating a normal pod bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Create, mynode), + err: "can only create mirror pods", + }, + { + name: "forbid update of normal pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of normal pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of normal pod status bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of normal pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of normal pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Normal pods not bound to any node + { + name: "forbid creating a normal pod unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, mynode), + err: "can only create mirror pods", + }, + { + name: "forbid update of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of normal pod status unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Missing pod + { + name: "forbid delete of unknown pod", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode), + err: "not found", + }, + + // Resource pods + { + name: "forbid create of pod referencing service account", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(sapod, nil, podKind, sapod.Namespace, sapod.Name, podResource, "", admission.Create, mynode), + err: "reference a service account", + }, + { + name: "forbid create of pod referencing secret", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(secretpod, nil, podKind, secretpod.Namespace, secretpod.Name, podResource, "", admission.Create, mynode), + err: "reference secrets", + }, + { + name: "forbid create of pod referencing configmap", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(configmappod, nil, podKind, configmappod.Namespace, configmappod.Name, podResource, "", admission.Create, mynode), + err: "reference configmaps", + }, + { + name: "forbid create of pod referencing persistentvolumeclaim", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(pvcpod, nil, podKind, pvcpod.Namespace, pvcpod.Name, podResource, "", admission.Create, mynode), + err: "reference persistentvolumeclaims", + }, + + // My node object + { + name: "allow create of my node", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Create, mynode), + err: "", + }, + { + name: "allow update of my node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode), + err: "", + }, + { + name: "allow delete of my node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Delete, mynode), + err: "", + }, + { + name: "allow update of my node status", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "status", admission.Update, mynode), + err: "", + }, + + // Other node object + { + name: "forbid create of other node", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Create, mynode), + err: "cannot modify other nodes", + }, + { + name: "forbid update of other node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Update, mynode), + err: "cannot modify other nodes", + }, + { + name: "forbid delete of other node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Delete, mynode), + err: "cannot modify other nodes", + }, + { + name: "forbid update of other node status", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "status", admission.Update, mynode), + err: "cannot modify other nodes", + }, + + // Unrelated objects + { + name: "allow create of unrelated object", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(&api.ConfigMap{}, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Create, mynode), + err: "", + }, + { + name: "allow update of unrelated object", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(&api.ConfigMap{}, &api.ConfigMap{}, configmapKind, "myns", "mycm", configmapResource, "", admission.Update, mynode), + err: "", + }, + { + name: "allow delete of unrelated object", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Delete, mynode), + err: "", + }, + + // Unrelated user + { + name: "allow unrelated user creating a normal pod unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, bob), + err: "", + }, + { + name: "allow unrelated user update of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, bob), + err: "", + }, + { + name: "allow unrelated user delete of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, bob), + err: "", + }, + { + name: "allow unrelated user create of normal pod status unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, bob), + err: "", + }, + { + name: "allow unrelated user update of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, bob), + err: "", + }, + { + name: "allow unrelated user delete of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, bob), + err: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier(), tt.strict) + c.podsGetter = tt.podsGetter + err := c.Admit(tt.attributes) + if (err == nil) != (len(tt.err) == 0) { + t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err) + return + } + if len(tt.err) > 0 && !strings.Contains(err.Error(), tt.err) { + t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err) + } + }) + } +} diff --git a/plugin/pkg/admission/persistentvolume/label/admission.go b/plugin/pkg/admission/persistentvolume/label/admission.go index 5bb711d1de7..50ad3018dbc 100644 --- a/plugin/pkg/admission/persistentvolume/label/admission.go +++ b/plugin/pkg/admission/persistentvolume/label/admission.go @@ -33,7 +33,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("PersistentVolumeLabel", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("PersistentVolumeLabel", func(config io.Reader) (admission.Interface, error) { persistentVolumeLabelAdmission := NewPersistentVolumeLabel() return persistentVolumeLabelAdmission, nil }) diff --git a/plugin/pkg/admission/podnodeselector/admission.go b/plugin/pkg/admission/podnodeselector/admission.go index fdcda7eda80..2def9ed1eea 100644 --- a/plugin/pkg/admission/podnodeselector/admission.go +++ b/plugin/pkg/admission/podnodeselector/admission.go @@ -40,7 +40,12 @@ import ( var NamespaceNodeSelectors = []string{"scheduler.alpha.kubernetes.io/node-selector"} func init() { - kubeapiserveradmission.Plugins.Register("PodNodeSelector", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("PodNodeSelector", func(config io.Reader) (admission.Interface, error) { // TODO move this to a versioned configuration file format. pluginConfig := readConfig(config) plugin := NewPodNodeSelector(pluginConfig.PodNodeSelectorPluginConfig) diff --git a/plugin/pkg/admission/podpreset/admission.go b/plugin/pkg/admission/podpreset/admission.go index 5948de35866..416c5221b01 100644 --- a/plugin/pkg/admission/podpreset/admission.go +++ b/plugin/pkg/admission/podpreset/admission.go @@ -42,7 +42,12 @@ const ( ) func init() { - kubeapiserveradmission.Plugins.Register(pluginName, func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(pluginName, func(config io.Reader) (admission.Interface, error) { return NewPlugin(), nil }) } diff --git a/plugin/pkg/admission/podtolerationrestriction/admission.go b/plugin/pkg/admission/podtolerationrestriction/admission.go index 04497436858..6c2b5f0920d 100644 --- a/plugin/pkg/admission/podtolerationrestriction/admission.go +++ b/plugin/pkg/admission/podtolerationrestriction/admission.go @@ -37,6 +37,11 @@ import ( ) func init() { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { kubeapiserveradmission.Plugins.Register("PodTolerationRestriction", func(config io.Reader) (admission.Interface, error) { pluginConfig, err := loadConfiguration(config) if err != nil { diff --git a/plugin/pkg/admission/resourcequota/admission.go b/plugin/pkg/admission/resourcequota/admission.go index 949cde03b4d..0d9f976d287 100644 --- a/plugin/pkg/admission/resourcequota/admission.go +++ b/plugin/pkg/admission/resourcequota/admission.go @@ -33,7 +33,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("ResourceQuota", + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("ResourceQuota", func(config io.Reader) (admission.Interface, error) { // load the configuration provided (if any) configuration, err := LoadConfiguration(config) diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission.go b/plugin/pkg/admission/security/podsecuritypolicy/admission.go index 012e474aa85..d1795a3326f 100644 --- a/plugin/pkg/admission/security/podsecuritypolicy/admission.go +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission.go @@ -45,7 +45,12 @@ const ( ) func init() { - kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { plugin := NewPlugin(psp.NewSimpleStrategyFactory(), getMatchingPolicies, true) return plugin, nil }) diff --git a/plugin/pkg/admission/securitycontext/scdeny/admission.go b/plugin/pkg/admission/securitycontext/scdeny/admission.go index 443cefab681..9f709c3bb3a 100644 --- a/plugin/pkg/admission/securitycontext/scdeny/admission.go +++ b/plugin/pkg/admission/securitycontext/scdeny/admission.go @@ -27,7 +27,12 @@ import ( ) func init() { - kubeapiserveradmission.Plugins.Register("SecurityContextDeny", func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register("SecurityContextDeny", func(config io.Reader) (admission.Interface, error) { return NewSecurityContextDeny(), nil }) } diff --git a/plugin/pkg/admission/serviceaccount/admission.go b/plugin/pkg/admission/serviceaccount/admission.go index 2c21f5d0105..17f35268725 100644 --- a/plugin/pkg/admission/serviceaccount/admission.go +++ b/plugin/pkg/admission/serviceaccount/admission.go @@ -54,7 +54,12 @@ const DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount" const PluginName = "ServiceAccount" func init() { - kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { serviceAccountAdmission := NewServiceAccount() return serviceAccountAdmission, nil }) diff --git a/plugin/pkg/admission/storageclass/default/admission.go b/plugin/pkg/admission/storageclass/default/admission.go index 568961badaa..d22e13ed5b8 100644 --- a/plugin/pkg/admission/storageclass/default/admission.go +++ b/plugin/pkg/admission/storageclass/default/admission.go @@ -39,7 +39,12 @@ const ( ) func init() { - kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + Register(&kubeapiserveradmission.Plugins) +} + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { plugin := newPlugin() return plugin, nil }) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go b/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go index 863ad98b417..88a363785d6 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/metrics/metrics.go @@ -103,10 +103,11 @@ func InstrumentRouteFunc(verb, resource string, routeFunc restful.RouteFunction) routeFunc(request, response) + reportedVerb := verb if verb == "LIST" && strings.ToLower(request.QueryParameter("watch")) == "true" { - verb = "WATCH" + reportedVerb = "WATCH" } - Monitor(&verb, &resource, cleanUserAgent(utilnet.GetHTTPClient(request.Request)), rw.Header().Get("Content-Type"), delegate.status, now) + Monitor(&reportedVerb, &resource, cleanUserAgent(utilnet.GetHTTPClient(request.Request)), rw.Header().Get("Content-Type"), delegate.status, now) }) } diff --git a/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-apiservice.yaml b/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-apiservice.yaml index 148ec4ef339..a11b3048a80 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-apiservice.yaml +++ b/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-apiservice.yaml @@ -1,4 +1,4 @@ -apiVersion: apiregistration.k8s.io/v1alpha1 +apiVersion: apiregistration.k8s.io/v1beta1 kind: APIService metadata: name: v1alpha1.mygroup.example.com diff --git a/staging/src/k8s.io/kube-apiextensions-server/artifacts/example/apiservice.yaml b/staging/src/k8s.io/kube-apiextensions-server/artifacts/example/apiservice.yaml index 3ede9152ac6..820a5fa598c 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/artifacts/example/apiservice.yaml +++ b/staging/src/k8s.io/kube-apiextensions-server/artifacts/example/apiservice.yaml @@ -1,4 +1,4 @@ -apiVersion: apiregistration.k8s.io/v1alpha1 +apiVersion: apiregistration.k8s.io/v1beta1 kind: APIService metadata: name: v1alpha1.apiextensions.k8s.io diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD index 0e128d1a316..c424286b114 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD @@ -5,6 +5,7 @@ licenses(["notice"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( @@ -24,3 +25,11 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", ], ) + +go_test( + name = "go_default_test", + srcs = ["helpers_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library"], +) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go index b495fd55d6f..c5731fca966 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go @@ -18,10 +18,10 @@ package apiextensions // SetCRDCondition sets the status condition. It either overwrites the existing one or // creates a new one -func SetCRDCondition(customResourceDefinition *CustomResourceDefinition, newCondition CustomResourceDefinitionCondition) { - existingCondition := FindCRDCondition(customResourceDefinition, newCondition.Type) +func SetCRDCondition(crd *CustomResourceDefinition, newCondition CustomResourceDefinitionCondition) { + existingCondition := FindCRDCondition(crd, newCondition.Type) if existingCondition == nil { - customResourceDefinition.Status.Conditions = append(customResourceDefinition.Status.Conditions, newCondition) + crd.Status.Conditions = append(crd.Status.Conditions, newCondition) return } @@ -34,11 +34,22 @@ func SetCRDCondition(customResourceDefinition *CustomResourceDefinition, newCond existingCondition.Message = newCondition.Message } +// RemoveCRDCondition removes the status condition. +func RemoveCRDCondition(crd *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) { + newConditions := []CustomResourceDefinitionCondition{} + for _, condition := range crd.Status.Conditions { + if condition.Type != conditionType { + newConditions = append(newConditions, condition) + } + } + crd.Status.Conditions = newConditions +} + // FindCRDCondition returns the condition you're looking for or nil -func FindCRDCondition(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) *CustomResourceDefinitionCondition { - for i := range customResourceDefinition.Status.Conditions { - if customResourceDefinition.Status.Conditions[i].Type == conditionType { - return &customResourceDefinition.Status.Conditions[i] +func FindCRDCondition(crd *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) *CustomResourceDefinitionCondition { + for i := range crd.Status.Conditions { + if crd.Status.Conditions[i].Type == conditionType { + return &crd.Status.Conditions[i] } } @@ -46,18 +57,18 @@ func FindCRDCondition(customResourceDefinition *CustomResourceDefinition, condit } // IsCRDConditionTrue indicates if the condition is present and strictly true -func IsCRDConditionTrue(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) bool { - return IsCRDConditionPresentAndEqual(customResourceDefinition, conditionType, ConditionTrue) +func IsCRDConditionTrue(crd *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) bool { + return IsCRDConditionPresentAndEqual(crd, conditionType, ConditionTrue) } // IsCRDConditionFalse indicates if the condition is present and false true -func IsCRDConditionFalse(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) bool { - return IsCRDConditionPresentAndEqual(customResourceDefinition, conditionType, ConditionFalse) +func IsCRDConditionFalse(crd *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) bool { + return IsCRDConditionPresentAndEqual(crd, conditionType, ConditionFalse) } // IsCRDConditionPresentAndEqual indicates if the condition is present and equal to the arg -func IsCRDConditionPresentAndEqual(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType, status ConditionStatus) bool { - for _, condition := range customResourceDefinition.Status.Conditions { +func IsCRDConditionPresentAndEqual(crd *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType, status ConditionStatus) bool { + for _, condition := range crd.Status.Conditions { if condition.Type == conditionType { return condition.Status == status } @@ -76,3 +87,25 @@ func IsCRDConditionEquivalent(lhs, rhs *CustomResourceDefinitionCondition) bool return lhs.Message == rhs.Message && lhs.Reason == rhs.Reason && lhs.Status == rhs.Status && lhs.Type == rhs.Type } + +// CRDHasFinalizer returns true if the finalizer is in the list +func CRDHasFinalizer(crd *CustomResourceDefinition, needle string) bool { + for _, finalizer := range crd.Finalizers { + if finalizer == needle { + return true + } + } + + return false +} + +// CRDRemoveFinalizer removes the finalizer if present +func CRDRemoveFinalizer(crd *CustomResourceDefinition, needle string) { + newFinalizers := []string{} + for _, finalizer := range crd.Finalizers { + if finalizer != needle { + newFinalizers = append(newFinalizers, finalizer) + } + } + crd.Finalizers = newFinalizers +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go new file mode 100644 index 00000000000..40e65b59a86 --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2016 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 apiextensions + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCRDHasFinalizer(t *testing.T) { + tests := []struct { + name string + crd *CustomResourceDefinition + finalizerToCheck string + + expected bool + }{ + { + name: "missing", + crd: &CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it"}}, + }, + finalizerToCheck: "it", + expected: false, + }, + { + name: "present", + crd: &CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it", "it"}}, + }, + finalizerToCheck: "it", + expected: true, + }, + } + for _, tc := range tests { + actual := CRDHasFinalizer(tc.crd, tc.finalizerToCheck) + if tc.expected != actual { + t.Errorf("%v expected %v, got %v", tc.name, tc.expected, actual) + } + } +} + +func TestCRDRemoveFinalizer(t *testing.T) { + tests := []struct { + name string + crd *CustomResourceDefinition + finalizerToCheck string + + expected []string + }{ + { + name: "missing", + crd: &CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it"}}, + }, + finalizerToCheck: "it", + expected: []string{"not-it"}, + }, + { + name: "present", + crd: &CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it", "it"}}, + }, + finalizerToCheck: "it", + expected: []string{"not-it"}, + }, + } + for _, tc := range tests { + CRDRemoveFinalizer(tc.crd, tc.finalizerToCheck) + if !reflect.DeepEqual(tc.expected, tc.crd.Finalizers) { + t.Errorf("%v expected %v, got %v", tc.name, tc.expected, tc.crd.Finalizers) + } + } +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/types.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/types.go index f802f0b5d58..c2012df173a 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/types.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/types.go @@ -104,6 +104,10 @@ type CustomResourceDefinitionStatus struct { AcceptedNames CustomResourceDefinitionNames } +// CustomResourceCleanupFinalizer is the name of the finalizer which will delete instances of +// a CustomResourceDefinition +const CustomResourceCleanupFinalizer = "customresourcecleanup.apiextensions.k8s.io" + // +genclient=true // +nonNamespaced=true diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1/types.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1/types.go index c052e75079d..1195df29bb7 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1/types.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1/types.go @@ -104,6 +104,10 @@ type CustomResourceDefinitionStatus struct { AcceptedNames CustomResourceDefinitionNames `json:"acceptedNames" protobuf:"bytes,2,opt,name=acceptedNames"` } +// CustomResourceCleanupFinalizer is the name of the finalizer which will delete instances of +// a CustomResourceDefinition +const CustomResourceCleanupFinalizer = "customresourcecleanup.apiextensions.k8s.io" + // +genclient=true // +nonNamespaced=true diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD index 235e4f2263b..56a8d0a96af 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD @@ -43,6 +43,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library", "//vendor/k8s.io/client-go/discovery:go_default_library", + "//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library", "//vendor/k8s.io/client-go/util/workqueue:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", @@ -54,6 +55,7 @@ go_library( "//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/controller/finalizer:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/controller/status:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresource:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition:go_default_library", diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go index 008dee48466..341afccfc3b 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go @@ -31,12 +31,14 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/client-go/dynamic" "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/install" "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1" "k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset" internalinformers "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion" + "k8s.io/kube-apiextensions-server/pkg/controller/finalizer" "k8s.io/kube-apiextensions-server/pkg/controller/status" "k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition" @@ -157,6 +159,10 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) customResourceDefinitionController := NewDiscoveryController(customResourceDefinitionInformers.Apiextensions().InternalVersion().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler) namingController := status.NewNamingConditionController(customResourceDefinitionInformers.Apiextensions().InternalVersion().CustomResourceDefinitions(), customResourceDefinitionClient) + finalizingController := finalizer.NewCRDFinalizer( + customResourceDefinitionInformers.Apiextensions().InternalVersion().CustomResourceDefinitions(), + customResourceDefinitionClient, + dynamic.NewDynamicClientPool(s.GenericAPIServer.LoopbackClientConfig)) s.GenericAPIServer.AddPostStartHook("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error { customResourceDefinitionInformers.Start(context.StopCh) @@ -165,6 +171,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) s.GenericAPIServer.AddPostStartHook("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error { go customResourceDefinitionController.Run(context.StopCh) go namingController.Run(context.StopCh) + go finalizingController.Run(5, context.StopCh) return nil }) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go index 39cdb9c6ee3..3ba89146b29 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go @@ -101,12 +101,18 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { } foundVersion = true + verbs := metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}) + // if we're terminating we don't allow some verbs + if apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) { + verbs = metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "watch"}) + } + apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{ Name: crd.Status.AcceptedNames.Plural, SingularName: crd.Status.AcceptedNames.Singular, Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped, Kind: crd.Status.AcceptedNames.Kind, - Verbs: metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}), + Verbs: verbs, ShortNames: crd.Status.AcceptedNames.ShortNames, }) } diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go index 81feb20fcb4..6bad06de2be 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go @@ -104,13 +104,11 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if !ok { // programmer error panic("missing context") - return } requestInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok { // programmer error panic("missing requestInfo") - return } if !requestInfo.IsResourceRequest { pathParts := splitPath(requestInfo.Path) @@ -153,6 +151,8 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.delegate.ServeHTTP(w, req) } + terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) + crdInfo := r.getServingInfoFor(crd) storage := crdInfo.storage requestScope := crdInfo.requestScope @@ -174,14 +174,26 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { handler(w, req) return case "create": + if terminating { + http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) + return + } handler := handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission) handler(w, req) return case "update": + if terminating { + http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) + return + } handler := handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission) handler(w, req) return case "patch": + if terminating { + http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) + return + } handler := handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}) handler(w, req) return @@ -190,6 +202,11 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { handler := handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission) handler(w, req) return + case "deletecollection": + checkBody := true + handler := handlers.DeleteCollection(storage, checkBody, requestScope, r.admission) + handler(w, req) + return default: http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/finalizer/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/finalizer/BUILD new file mode 100644 index 00000000000..00b7e0ce951 --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/finalizer/BUILD @@ -0,0 +1,34 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["crd_finalizer.go"], + tags = ["automanaged"], + deps = [ + "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//vendor/k8s.io/client-go/dynamic:go_default_library", + "//vendor/k8s.io/client-go/tools/cache:go_default_library", + "//vendor/k8s.io/client-go/util/workqueue:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion:go_default_library", + ], +) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/finalizer/crd_finalizer.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/finalizer/crd_finalizer.go new file mode 100644 index 00000000000..2f6516d1db1 --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/finalizer/crd_finalizer.go @@ -0,0 +1,333 @@ +/* +Copyright 2017 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 finalizer + +import ( + "fmt" + "reflect" + "time" + + "github.com/golang/glog" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" + client "k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion" + informers "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion" + listers "k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion" +) + +var cloner = conversion.NewCloner() + +// This controller finalizes the CRD by deleting all the CRs associated with it. +type CRDFinalizer struct { + crdClient client.CustomResourceDefinitionsGetter + // clientPool is a dynamic client used to delete the individual instances + clientPool dynamic.ClientPool + + crdLister listers.CustomResourceDefinitionLister + crdSynced cache.InformerSynced + + // To allow injection for testing. + syncFn func(key string) error + + queue workqueue.RateLimitingInterface +} + +func NewCRDFinalizer( + crdInformer informers.CustomResourceDefinitionInformer, + crdClient client.CustomResourceDefinitionsGetter, + clientPool dynamic.ClientPool, +) *CRDFinalizer { + c := &CRDFinalizer{ + crdClient: crdClient, + clientPool: clientPool, + crdLister: crdInformer.Lister(), + crdSynced: crdInformer.Informer().HasSynced, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "CustomResourceDefinition-CRDFinalizer"), + } + + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.addCustomResourceDefinition, + UpdateFunc: c.updateCustomResourceDefinition, + }) + + c.syncFn = c.sync + + return c +} + +func (c *CRDFinalizer) sync(key string) error { + cachedCRD, err := c.crdLister.Get(key) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + // no work to do + if cachedCRD.DeletionTimestamp.IsZero() || !apiextensions.CRDHasFinalizer(cachedCRD, apiextensions.CustomResourceCleanupFinalizer) { + return nil + } + + crd := &apiextensions.CustomResourceDefinition{} + if err := apiextensions.DeepCopy_apiextensions_CustomResourceDefinition(cachedCRD, crd, cloner); err != nil { + return err + } + + // update the status condition. This cleanup could take a while. + apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.Terminating, + Status: apiextensions.ConditionTrue, + Reason: "InstanceDeletionInProgress", + Message: "CustomResource deletion is in progress", + }) + crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if err != nil { + return err + } + + // Its possible for a naming conflict to have removed this resource from the API after instances were created. + // For now we will cowardly stop finalizing. If we don't go through the REST API, weird things may happen: + // no audit trail, no admission checks or side effects, finalization would probably still work but defaulting + // would be missed. It would be a mess. + // This requires human intervention to solve, update status so they have a reason. + // TODO split coreNamesAccepted from extendedNamesAccepted. If coreNames were accepted, then we have something to cleanup + // and the endpoint is serviceable. if they aren't, then there's nothing to cleanup. + if !apiextensions.IsCRDConditionFalse(crd, apiextensions.NameConflict) { + apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.Terminating, + Status: apiextensions.ConditionTrue, + Reason: "InstanceDeletionStuck", + Message: fmt.Sprintf("cannot proceed with deletion because of %v condition", apiextensions.NameConflict), + }) + crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if err != nil { + return err + } + return fmt.Errorf("cannot proceed with deletion because of %v condition", apiextensions.NameConflict) + } + + // Now we can start deleting items. We should use the REST API to ensure that all normal admission runs. + // Since we control the endpoints, we know that delete collection works. + crClient, err := c.clientPool.ClientForGroupVersionResource(schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural}) + if err != nil { + return err + } + crAPIResource := &metav1.APIResource{ + Name: crd.Status.AcceptedNames.Plural, + SingularName: crd.Status.AcceptedNames.Singular, + Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped, + Kind: crd.Status.AcceptedNames.Kind, + Verbs: metav1.Verbs([]string{"deletecollection", "list"}), + ShortNames: crd.Status.AcceptedNames.ShortNames, + } + crResourceClient := crClient.Resource(crAPIResource, "" /* namespace all */) + allResources, err := crResourceClient.List(metav1.ListOptions{}) + if err != nil { + return err + } + + deletedNamespaces := sets.String{} + deleteErrors := []error{} + for _, item := range allResources.(*unstructured.UnstructuredList).Items { + metadata, err := meta.Accessor(&item) + if err != nil { + utilruntime.HandleError(err) + continue + } + if deletedNamespaces.Has(metadata.GetNamespace()) { + continue + } + // don't retry deleting the same namespace + deletedNamespaces.Insert(metadata.GetNamespace()) + if err := crClient.Resource(crAPIResource, metadata.GetNamespace()).DeleteCollection(nil, metav1.ListOptions{}); err != nil { + deleteErrors = append(deleteErrors, err) + continue + } + } + if deleteError := utilerrors.NewAggregate(deleteErrors); deleteError != nil { + apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.Terminating, + Status: apiextensions.ConditionTrue, + Reason: "InstanceDeletionFailed", + Message: fmt.Sprintf("could not issue all deletes: %v", deleteError), + }) + crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if err != nil { + utilruntime.HandleError(err) + } + return deleteError + } + + // now we need to wait until all the resources are deleted. Start with a simple poll before we do anything fancy. + // TODO not all servers are synchronized on caches. It is possible for a stale one to still be creating things. + // Once we have a mechanism for servers to indicate their states, we should check that for concurrence. + listErr := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) { + listObj, err := crResourceClient.List(metav1.ListOptions{}) + if err != nil { + return false, err + } + if len(listObj.(*unstructured.UnstructuredList).Items) == 0 { + return true, nil + } + glog.V(2).Infof("%s.%s waiting for %d items to be removed", crd.Status.AcceptedNames.Plural, crd.Spec.Group, len(listObj.(*unstructured.UnstructuredList).Items)) + return false, nil + }) + if listErr != nil { + apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.Terminating, + Status: apiextensions.ConditionTrue, + Reason: "InstanceDeletionCheck", + Message: fmt.Sprintf("could not confirm zero CustomResources remaining: %v", listErr), + }) + crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if err != nil { + utilruntime.HandleError(err) + } + return listErr + } + + apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.Terminating, + Status: apiextensions.ConditionFalse, + Reason: "InstanceDeletionCompleted", + Message: "removed all instances", + }) + apiextensions.CRDRemoveFinalizer(crd, apiextensions.CustomResourceCleanupFinalizer) + crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if err != nil { + return err + } + + // and now issue another delete, which should clean it all up if no finalizers remain or no-op if they do + return c.crdClient.CustomResourceDefinitions().Delete(crd.Name, nil) +} + +func (c *CRDFinalizer) Run(workers int, stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + defer c.queue.ShutDown() + + glog.Infof("Starting CRDFinalizer") + defer glog.Infof("Shutting down CRDFinalizer") + + if !cache.WaitForCacheSync(stopCh, c.crdSynced) { + return + } + + for i := 0; i < workers; i++ { + go wait.Until(c.runWorker, time.Second, stopCh) + } + + <-stopCh +} + +func (c *CRDFinalizer) runWorker() { + for c.processNextWorkItem() { + } +} + +// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit. +func (c *CRDFinalizer) processNextWorkItem() bool { + key, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(key) + + err := c.syncFn(key.(string)) + if err == nil { + c.queue.Forget(key) + return true + } + + utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err)) + c.queue.AddRateLimited(key) + + return true +} + +func (c *CRDFinalizer) enqueue(obj *apiextensions.CustomResourceDefinition) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", obj, err)) + return + } + + c.queue.Add(key) +} + +func (c *CRDFinalizer) addCustomResourceDefinition(obj interface{}) { + castObj := obj.(*apiextensions.CustomResourceDefinition) + // only queue deleted things + if !castObj.DeletionTimestamp.IsZero() && apiextensions.CRDHasFinalizer(castObj, apiextensions.CustomResourceCleanupFinalizer) { + c.enqueue(castObj) + } +} + +func (c *CRDFinalizer) updateCustomResourceDefinition(oldObj, newObj interface{}) { + oldCRD := oldObj.(*apiextensions.CustomResourceDefinition) + newCRD := newObj.(*apiextensions.CustomResourceDefinition) + // only queue deleted things that haven't been finalized by us + if newCRD.DeletionTimestamp.IsZero() || !apiextensions.CRDHasFinalizer(newCRD, apiextensions.CustomResourceCleanupFinalizer) { + return + } + + // always requeue resyncs just in case + if oldCRD.ResourceVersion == newCRD.ResourceVersion { + c.enqueue(newCRD) + return + } + + // If the only difference is in the terminating condition, then there's no reason to requeue here. This controller + // is likely to be the originator, so requeuing would hot-loop us. Failures are requeued by the workqueue directly. + // This is a low traffic and scale resource, so the copy is terrible. It's not good, so better ideas + // are welcome. + oldCopy := &apiextensions.CustomResourceDefinition{} + if err := apiextensions.DeepCopy_apiextensions_CustomResourceDefinition(oldCRD, oldCopy, cloner); err != nil { + utilruntime.HandleError(err) + c.enqueue(newCRD) + return + } + newCopy := &apiextensions.CustomResourceDefinition{} + if err := apiextensions.DeepCopy_apiextensions_CustomResourceDefinition(newCRD, newCopy, cloner); err != nil { + utilruntime.HandleError(err) + c.enqueue(newCRD) + return + } + oldCopy.ResourceVersion = "" + newCopy.ResourceVersion = "" + apiextensions.RemoveCRDCondition(oldCopy, apiextensions.Terminating) + apiextensions.RemoveCRDCondition(newCopy, apiextensions.Terminating) + + if !reflect.DeepEqual(oldCopy, newCopy) { + c.enqueue(newCRD) + } +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD index ade49d817d3..59f4a682cc5 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD @@ -15,6 +15,8 @@ go_library( ], tags = ["automanaged"], deps = [ + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", @@ -24,6 +26,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/errors:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation:go_default_library", diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go index 74c4f08b3e7..63bcd93ba48 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go @@ -17,11 +17,17 @@ limitations under the License. package customresourcedefinition import ( + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/storage" + storageerr "k8s.io/apiserver/pkg/storage/errors" "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" ) @@ -52,6 +58,87 @@ func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) *REST return &REST{store} } +// Delete adds the CRD finalizer to the list +func (r *REST) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + obj, err := r.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + + crd := obj.(*apiextensions.CustomResourceDefinition) + + // Ensure we have a UID precondition + if options == nil { + options = metav1.NewDeleteOptions(0) + } + if options.Preconditions == nil { + options.Preconditions = &metav1.Preconditions{} + } + if options.Preconditions.UID == nil { + options.Preconditions.UID = &crd.UID + } else if *options.Preconditions.UID != crd.UID { + err = apierrors.NewConflict( + apiextensions.Resource("customresourcedefinitions"), + name, + fmt.Errorf("Precondition failed: UID in precondition: %v, UID in object meta: %v", *options.Preconditions.UID, crd.UID), + ) + return nil, false, err + } + + // upon first request to delete, add our finalizer and then delegate + if crd.DeletionTimestamp.IsZero() { + key, err := r.Store.KeyFunc(ctx, name) + if err != nil { + return nil, false, err + } + + preconditions := storage.Preconditions{UID: options.Preconditions.UID} + + out := r.Store.NewFunc() + err = r.Store.Storage.GuaranteedUpdate( + ctx, key, out, false, &preconditions, + storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) { + existingCRD, ok := existing.(*apiextensions.CustomResourceDefinition) + if !ok { + // wrong type + return nil, fmt.Errorf("expected *apiextensions.CustomResourceDefinition, got %v", existing) + } + + // Set the deletion timestamp if needed + if existingCRD.DeletionTimestamp.IsZero() { + now := metav1.Now() + existingCRD.DeletionTimestamp = &now + } + + if !apiextensions.CRDHasFinalizer(existingCRD, apiextensions.CustomResourceCleanupFinalizer) { + existingCRD.Finalizers = append(existingCRD.Finalizers, apiextensions.CustomResourceCleanupFinalizer) + } + // update the status condition too + apiextensions.SetCRDCondition(existingCRD, apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.Terminating, + Status: apiextensions.ConditionTrue, + Reason: "InstanceDeletionPending", + Message: "CustomResourceDefinition marked for deletion; CustomResource deletion will begin soon", + }) + return existingCRD, nil + }), + ) + + if err != nil { + err = storageerr.InterpretGetError(err, apiextensions.Resource("customresourcedefinitions"), name) + err = storageerr.InterpretUpdateError(err, apiextensions.Resource("customresourcedefinitions"), name) + if _, ok := err.(*apierrors.StatusError); !ok { + err = apierrors.NewInternalError(err) + } + return nil, false, err + } + + return out, false, nil + } + + return r.Store.Delete(ctx, name, options) +} + // NewStatusREST makes a RESTStorage for status that has more limited options. // It is based on the original REST so that we can share the same underlying store func NewStatusREST(scheme *runtime.Scheme, rest *REST) *StatusREST { diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go index c29fe8144c2..07e77c5204d 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go @@ -89,7 +89,6 @@ func (statusStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old r newObj.Spec = oldObj.Spec newObj.Labels = oldObj.Labels newObj.Annotations = oldObj.Annotations - newObj.Finalizers = oldObj.Finalizers newObj.OwnerReferences = oldObj.OwnerReferences } diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD b/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD index dca4da9927d..ae598a317a8 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD @@ -23,6 +23,7 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library", diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go b/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go index 659bd4ee717..c176e6da52e 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" apiextensionsv1alpha1 "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1" @@ -333,3 +334,48 @@ func TestSelfLink(t *testing.T) { // TODO add test for cluster scoped self-link when its available } + +func TestPreserveInt(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1alpha1.ClusterScoped) + noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + ns := "not-the-default" + noxuNamespacedResourceClient := noxuVersionClient.Resource(&metav1.APIResource{ + Name: noxuDefinition.Spec.Names.Plural, + Namespaced: true, + }, ns) + + noxuInstanceToCreate := testserver.NewNoxuInstance(ns, "foo") + createdNoxuInstance, err := noxuNamespacedResourceClient.Create(noxuInstanceToCreate) + if err != nil { + t.Fatal(err) + } + + originalJSON, err := runtime.Encode(unstructured.UnstructuredJSONScheme, createdNoxuInstance) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + gottenNoxuInstance, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalJSON) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check if int is preserved. + unstructuredObj := gottenNoxuInstance.(*unstructured.Unstructured).Object + num := unstructuredObj["num"].(map[string]interface{}) + num1 := num["num1"].(int64) + num2 := num["num2"].(int64) + if num1 != 9223372036854775807 || num2 != 1000000 { + t.Errorf("Expected %v, got %v, %v", `9223372036854775807, 1000000`, num1, num2) + } +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/registration_test.go b/staging/src/k8s.io/kube-apiextensions-server/test/integration/registration_test.go index 22edc8f3be2..9505723e5d0 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/registration_test.go +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/registration_test.go @@ -271,11 +271,6 @@ func TestDeRegistrationAndReRegistration(t *testing.T) { if _, err := instantiateCustomResource(t, testserver.NewNoxuInstance(ns, sameInstanceName), noxuNamespacedResourceClient, noxuDefinition); err != nil { t.Fatal(err) } - // Remove sameInstanceName since at the moment there's no finalizers. - // TODO: as soon finalizers will be implemented Delete can be removed. - if err := noxuNamespacedResourceClient.Delete(sameInstanceName, nil); err != nil { - t.Fatal(err) - } if err := testserver.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go b/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go index 099c82c1d99..519cb5e9d86 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go @@ -59,6 +59,10 @@ func NewNoxuInstance(namespace, name string) *unstructured.Unstructured { "content": map[string]interface{}{ "key": "value", }, + "num": map[string]interface{}{ + "num1": 9223372036854775807, + "num2": 1000000, + }, }, } } diff --git a/test/e2e_node/security_context_test.go b/test/e2e_node/security_context_test.go index f45b97dbb12..ff600806c54 100644 --- a/test/e2e_node/security_context_test.go +++ b/test/e2e_node/security_context_test.go @@ -18,6 +18,7 @@ package e2e_node import ( "fmt" + "net" "os/exec" "strings" @@ -195,4 +196,82 @@ var _ = framework.KubeDescribe("Security Context", func() { } }) }) + + Context("when creating a pod in the host network namespace", func() { + makeHostNetworkPod := func(podName, image string, command []string, hostNetwork bool) *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + }, + Spec: v1.PodSpec{ + RestartPolicy: v1.RestartPolicyNever, + HostNetwork: hostNetwork, + Containers: []v1.Container{ + { + Image: image, + Name: podName, + Command: command, + }, + }, + }, + } + } + listListeningPortsCommand := []string{"sh", "-c", "netstat -ln"} + createAndWaitHostNetworkPod := func(podName string, hostNetwork bool) { + podClient.Create(makeHostNetworkPod(podName, + "gcr.io/google_containers/busybox:1.24", + listListeningPortsCommand, + hostNetwork, + )) + + podClient.WaitForSuccess(podName, framework.PodStartTimeout) + } + + listeningPort := "" + var l net.Listener + BeforeEach(func() { + l, err := net.Listen("tcp", ":0") + if err != nil { + framework.Failf("Failed to open a new tcp port: %v", err) + } + addr := strings.Split(l.Addr().String(), ":") + listeningPort = addr[len(addr)-1] + framework.Logf("Opened a new tcp port %q", listeningPort) + }) + + It("should listen on same port in the host network containers", func() { + busyboxPodName := "busybox-hostnetwork-" + string(uuid.NewUUID()) + createAndWaitHostNetworkPod(busyboxPodName, true) + logs, err := framework.GetPodLogs(f.ClientSet, f.Namespace.Name, busyboxPodName, busyboxPodName) + if err != nil { + framework.Failf("GetPodLogs for pod %q failed: %v", busyboxPodName, err) + } + + framework.Logf("Got logs for pod %q: %q", busyboxPodName, logs) + if !strings.Contains(logs, listeningPort) { + framework.Failf("host-networked container should listening on same port as host") + } + }) + + It("shouldn't show the same port in the non-hostnetwork containers", func() { + busyboxPodName := "busybox-non-hostnetwork-" + string(uuid.NewUUID()) + createAndWaitHostNetworkPod(busyboxPodName, false) + logs, err := framework.GetPodLogs(f.ClientSet, f.Namespace.Name, busyboxPodName, busyboxPodName) + if err != nil { + framework.Failf("GetPodLogs for pod %q failed: %v", busyboxPodName, err) + } + + framework.Logf("Got logs for pod %q: %q", busyboxPodName, logs) + if strings.Contains(logs, listeningPort) { + framework.Failf("non-hostnetworked container shouldn't show the same port as host") + } + }) + + AfterEach(func() { + if l != nil { + l.Close() + } + }) + }) + })