From 48a9357926f906af581602b1f79ad43fef42e0a4 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Fri, 5 May 2017 02:14:27 -0400 Subject: [PATCH] Add PV util for extracting referenced secrets --- hack/.linted_packages | 1 + pkg/api/BUILD | 1 + pkg/api/persistentvolume/BUILD | 41 ++++++++ pkg/api/persistentvolume/OWNERS | 4 + pkg/api/persistentvolume/util.go | 55 +++++++++++ pkg/api/persistentvolume/util_test.go | 133 ++++++++++++++++++++++++++ 6 files changed, 235 insertions(+) create mode 100644 pkg/api/persistentvolume/BUILD create mode 100755 pkg/api/persistentvolume/OWNERS create mode 100644 pkg/api/persistentvolume/util.go create mode 100644 pkg/api/persistentvolume/util_test.go diff --git a/hack/.linted_packages b/hack/.linted_packages index f0f818422d..6f78760548 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -59,6 +59,7 @@ pkg/api/errors pkg/api/events pkg/api/install pkg/api/meta +pkg/api/persistentvolume pkg/api/pod pkg/api/resource pkg/api/service diff --git a/pkg/api/BUILD b/pkg/api/BUILD index c4ca2b6339..ee99308203 100644 --- a/pkg/api/BUILD +++ b/pkg/api/BUILD @@ -107,6 +107,7 @@ filegroup( "//pkg/api/helper:all-srcs", "//pkg/api/install:all-srcs", "//pkg/api/meta:all-srcs", + "//pkg/api/persistentvolume:all-srcs", "//pkg/api/pod:all-srcs", "//pkg/api/ref:all-srcs", "//pkg/api/resource:all-srcs", diff --git a/pkg/api/persistentvolume/BUILD b/pkg/api/persistentvolume/BUILD new file mode 100644 index 0000000000..1699f870c3 --- /dev/null +++ b/pkg/api/persistentvolume/BUILD @@ -0,0 +1,41 @@ +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 = ["util.go"], + tags = ["automanaged"], + deps = ["//pkg/api:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) + +go_test( + name = "go_default_test", + srcs = ["util_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) diff --git a/pkg/api/persistentvolume/OWNERS b/pkg/api/persistentvolume/OWNERS new file mode 100755 index 0000000000..b74bd2bf49 --- /dev/null +++ b/pkg/api/persistentvolume/OWNERS @@ -0,0 +1,4 @@ +reviewers: +- smarterclayton +- kargakis +- david-mcmahon diff --git a/pkg/api/persistentvolume/util.go b/pkg/api/persistentvolume/util.go new file mode 100644 index 0000000000..b33077d68d --- /dev/null +++ b/pkg/api/persistentvolume/util.go @@ -0,0 +1,55 @@ +/* +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 persistentvolume + +import ( + "k8s.io/kubernetes/pkg/api" +) + +// VisitPVSecretNames invokes the visitor function with the name of every secret +// referenced by the PV spec. If visitor returns false, visiting is short-circuited. +// Returns true if visiting completed, false if visiting was short-circuited. +func VisitPVSecretNames(pv *api.PersistentVolume, visitor func(string) bool) bool { + source := &pv.Spec.PersistentVolumeSource + switch { + case source.AzureFile != nil: + if len(source.AzureFile.SecretName) > 0 && !visitor(source.AzureFile.SecretName) { + return false + } + case source.CephFS != nil: + if source.CephFS.SecretRef != nil && !visitor(source.CephFS.SecretRef.Name) { + return false + } + case source.FlexVolume != nil: + if source.FlexVolume.SecretRef != nil && !visitor(source.FlexVolume.SecretRef.Name) { + return false + } + case source.RBD != nil: + if source.RBD.SecretRef != nil && !visitor(source.RBD.SecretRef.Name) { + return false + } + case source.ScaleIO != nil: + if source.ScaleIO.SecretRef != nil && !visitor(source.ScaleIO.SecretRef.Name) { + return false + } + case source.ISCSI != nil: + if source.ISCSI.SecretRef != nil && !visitor(source.ISCSI.SecretRef.Name) { + return false + } + } + return true +} diff --git a/pkg/api/persistentvolume/util_test.go b/pkg/api/persistentvolume/util_test.go new file mode 100644 index 0000000000..f8353050de --- /dev/null +++ b/pkg/api/persistentvolume/util_test.go @@ -0,0 +1,133 @@ +/* +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 persistentvolume + +import ( + "reflect" + "testing" + + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kubernetes/pkg/api" +) + +func TestPVSecrets(t *testing.T) { + // Stub containing all possible secret references in a PV. + // The names of the referenced secrets match struct paths detected by reflection. + pvs := []*api.PersistentVolume{ + {Spec: api.PersistentVolumeSpec{PersistentVolumeSource: api.PersistentVolumeSource{ + AzureFile: &api.AzureFileVolumeSource{ + SecretName: "Spec.PersistentVolumeSource.AzureFile.SecretName"}}}}, + {Spec: api.PersistentVolumeSpec{PersistentVolumeSource: api.PersistentVolumeSource{ + CephFS: &api.CephFSVolumeSource{ + SecretRef: &api.LocalObjectReference{ + Name: "Spec.PersistentVolumeSource.CephFS.SecretRef"}}}}}, + {Spec: api.PersistentVolumeSpec{PersistentVolumeSource: api.PersistentVolumeSource{ + FlexVolume: &api.FlexVolumeSource{ + SecretRef: &api.LocalObjectReference{ + Name: "Spec.PersistentVolumeSource.FlexVolume.SecretRef"}}}}}, + {Spec: api.PersistentVolumeSpec{PersistentVolumeSource: api.PersistentVolumeSource{ + RBD: &api.RBDVolumeSource{ + SecretRef: &api.LocalObjectReference{ + Name: "Spec.PersistentVolumeSource.RBD.SecretRef"}}}}}, + {Spec: api.PersistentVolumeSpec{PersistentVolumeSource: api.PersistentVolumeSource{ + ScaleIO: &api.ScaleIOVolumeSource{ + SecretRef: &api.LocalObjectReference{ + Name: "Spec.PersistentVolumeSource.ScaleIO.SecretRef"}}}}}, + {Spec: api.PersistentVolumeSpec{PersistentVolumeSource: api.PersistentVolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + SecretRef: &api.LocalObjectReference{ + Name: "Spec.PersistentVolumeSource.ISCSI.SecretRef"}}}}}, + } + extractedNames := sets.NewString() + for _, pv := range pvs { + VisitPVSecretNames(pv, func(name string) bool { + extractedNames.Insert(name) + return true + }) + } + + // excludedSecretPaths holds struct paths to fields with "secret" in the name that are not actually references to secret API objects + excludedSecretPaths := sets.NewString( + "Spec.PersistentVolumeSource.CephFS.SecretFile", + ) + // expectedSecretPaths holds struct paths to fields with "secret" in the name that are references to secret API objects. + // every path here should be represented as an example in the PV stub above, with the secret name set to the path. + expectedSecretPaths := sets.NewString( + "Spec.PersistentVolumeSource.AzureFile.SecretName", + "Spec.PersistentVolumeSource.CephFS.SecretRef", + "Spec.PersistentVolumeSource.FlexVolume.SecretRef", + "Spec.PersistentVolumeSource.RBD.SecretRef", + "Spec.PersistentVolumeSource.ScaleIO.SecretRef", + "Spec.PersistentVolumeSource.ISCSI.SecretRef", + ) + secretPaths := collectSecretPaths(t, nil, "", reflect.TypeOf(&api.PersistentVolume{})) + secretPaths = secretPaths.Difference(excludedSecretPaths) + if missingPaths := expectedSecretPaths.Difference(secretPaths); len(missingPaths) > 0 { + t.Logf("Missing expected secret paths:\n%s", strings.Join(missingPaths.List(), "\n")) + t.Error("Missing expected secret paths. Verify VisitPVSecretNames() is correctly finding the missing paths, then correct expectedSecretPaths") + } + if extraPaths := secretPaths.Difference(expectedSecretPaths); len(extraPaths) > 0 { + t.Logf("Extra secret paths:\n%s", strings.Join(extraPaths.List(), "\n")) + t.Error("Extra fields with 'secret' in the name found. Verify VisitPVSecretNames() is including these fields if appropriate, then correct expectedSecretPaths") + } + + if missingNames := expectedSecretPaths.Difference(extractedNames); len(missingNames) > 0 { + t.Logf("Missing expected secret names:\n%s", strings.Join(missingNames.List(), "\n")) + t.Error("Missing expected secret names. Verify the PV stub above includes these references, then verify VisitPVSecretNames() is correctly finding the missing names") + } + if extraNames := extractedNames.Difference(expectedSecretPaths); len(extraNames) > 0 { + t.Logf("Extra secret names:\n%s", strings.Join(extraNames.List(), "\n")) + t.Error("Extra secret names extracted. Verify VisitPVSecretNames() is correctly extracting secret names") + } +} + +// collectSecretPaths traverses the object, computing all the struct paths that lead to fields with "secret" in the name. +func collectSecretPaths(t *testing.T, path *field.Path, name string, tp reflect.Type) sets.String { + secretPaths := sets.NewString() + + if tp.Kind() == reflect.Ptr { + secretPaths.Insert(collectSecretPaths(t, path, name, tp.Elem()).List()...) + return secretPaths + } + + if strings.Contains(strings.ToLower(name), "secret") { + secretPaths.Insert(path.String()) + } + + switch tp.Kind() { + case reflect.Ptr: + secretPaths.Insert(collectSecretPaths(t, path, name, tp.Elem()).List()...) + case reflect.Struct: + for i := 0; i < tp.NumField(); i++ { + field := tp.Field(i) + secretPaths.Insert(collectSecretPaths(t, path.Child(field.Name), field.Name, field.Type).List()...) + } + case reflect.Interface: + t.Errorf("cannot find secret fields in interface{} field %s", path.String()) + case reflect.Map: + secretPaths.Insert(collectSecretPaths(t, path.Key("*"), "", tp.Elem()).List()...) + case reflect.Slice: + secretPaths.Insert(collectSecretPaths(t, path.Key("*"), "", tp.Elem()).List()...) + default: + // all primitive types + } + + return secretPaths +}