Unverified Commit e568aa7f authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #55648 from mtaufen/kc-rel-paths

Automatic merge from submit-queue (batch tested with PRs 55648, 55274, 54982, 51955, 55639). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Kubelet: Relative paths in local config file Resolve relative paths against the config file's location. Issue: #55644 Related comment: https://github.com/kubernetes/kubernetes/pull/53833#issuecomment-344009912 Will add the same behavior for dynamic Kubelet config in a future PR, see issue #55645. ```release-note Relative paths in the Kubelet's local config files (--init-config-dir) will be resolved relative to the location of the containing files. ```
parents cdf4d8be 816174c6
...@@ -3,12 +3,14 @@ package(default_visibility = ["//visibility:public"]) ...@@ -3,12 +3,14 @@ package(default_visibility = ["//visibility:public"])
load( load(
"@io_bazel_rules_go//go:def.bzl", "@io_bazel_rules_go//go:def.bzl",
"go_library", "go_library",
"go_test",
) )
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"doc.go", "doc.go",
"helpers.go",
"register.go", "register.go",
"types.go", "types.go",
"zz_generated.deepcopy.go", "zz_generated.deepcopy.go",
...@@ -39,3 +41,14 @@ filegroup( ...@@ -39,3 +41,14 @@ filegroup(
], ],
tags = ["automanaged"], tags = ["automanaged"],
) )
go_test(
name = "go_default_test",
srcs = ["helpers_test.go"],
importpath = "k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig",
library = ":go_default_library",
deps = [
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
)
/*
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 kubeletconfig
// KubeletConfigurationPathRefs returns pointers to all of the KubeletConfiguration fields that contain filepaths.
// You might use this, for example, to resolve all relative paths against some common root before
// passing the configuration to the application. This method must be kept up to date as new fields are added.
func KubeletConfigurationPathRefs(kc *KubeletConfiguration) []*string {
paths := []*string{}
paths = append(paths, &kc.PodManifestPath)
paths = append(paths, &kc.Authentication.X509.ClientCAFile)
paths = append(paths, &kc.TLSCertFile)
paths = append(paths, &kc.TLSPrivateKeyFile)
paths = append(paths, &kc.SeccompProfileRoot)
paths = append(paths, &kc.ResolverConfig)
// TODO(#55562): planning on moving two out of KubeletConfiguration
paths = append(paths, &kc.VolumePluginDir)
paths = append(paths, &kc.LockFilePath)
return paths
}
/*
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 kubeletconfig
import (
"reflect"
"strings"
"testing"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestKubeletConfigurationPathFields(t *testing.T) {
// ensure the intersection of kubeletConfigurationPathFieldPaths and KubeletConfigurationNonPathFields is empty
if i := kubeletConfigurationPathFieldPaths.Intersection(kubeletConfigurationNonPathFieldPaths); len(i) > 0 {
t.Fatalf("expect the intersection of kubeletConfigurationPathFieldPaths and "+
"KubeletConfigurationNonPathFields to be emtpy, got:\n%s",
strings.Join(i.List(), "\n"))
}
// ensure that kubeletConfigurationPathFields U kubeletConfigurationNonPathFields == allPrimitiveFieldPaths(KubeletConfiguration)
expect := sets.NewString().Union(kubeletConfigurationPathFieldPaths).Union(kubeletConfigurationNonPathFieldPaths)
result := allPrimitiveFieldPaths(t, reflect.TypeOf(&KubeletConfiguration{}), nil)
if !expect.Equal(result) {
// expected fields missing from result
missing := expect.Difference(result)
// unexpected fields in result but not specified in expect
unexpected := result.Difference(expect)
if len(missing) > 0 {
t.Errorf("the following fields were expected, but missing from the result. "+
"If the field has been removed, please remove it from the kubeletConfigurationPathFieldPaths set "+
"and the KubeletConfigurationPathRefs function, "+
"or remove it from the kubeletConfigurationNonPathFieldPaths set, as appropriate:\n%s",
strings.Join(missing.List(), "\n"))
}
if len(unexpected) > 0 {
t.Errorf("the following fields were in the result, but unexpected. "+
"If the field is new, please add it to the kubeletConfigurationPathFieldPaths set "+
"and the KubeletConfigurationPathRefs function, "+
"or add it to the kubeletConfigurationNonPathFieldPaths set, as appropriate:\n%s",
strings.Join(unexpected.List(), "\n"))
}
}
}
func allPrimitiveFieldPaths(t *testing.T, tp reflect.Type, path *field.Path) sets.String {
paths := sets.NewString()
switch tp.Kind() {
case reflect.Ptr:
paths.Insert(allPrimitiveFieldPaths(t, tp.Elem(), path).List()...)
case reflect.Struct:
for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i)
paths.Insert(allPrimitiveFieldPaths(t, field.Type, path.Child(field.Name)).List()...)
}
case reflect.Map, reflect.Slice:
paths.Insert(allPrimitiveFieldPaths(t, tp.Elem(), path.Key("*")).List()...)
case reflect.Interface:
t.Fatalf("unexpected interface{} field %s", path.String())
default:
// if we hit a primitive type, we're at a leaf
paths.Insert(path.String())
}
return paths
}
// dummy helper types
type foo struct {
foo int
}
type bar struct {
str string
strptr *string
ints []int
stringMap map[string]string
foo foo
fooptr *foo
bars []foo
barMap map[string]foo
}
func TestAllPrimitiveFieldPaths(t *testing.T) {
expect := sets.NewString(
"str",
"strptr",
"ints[*]",
"stringMap[*]",
"foo.foo",
"fooptr.foo",
"bars[*].foo",
"barMap[*].foo",
)
result := allPrimitiveFieldPaths(t, reflect.TypeOf(&bar{}), nil)
if !expect.Equal(result) {
// expected fields missing from result
missing := expect.Difference(result)
// unexpected fields in result but not specified in expect
unexpected := result.Difference(expect)
if len(missing) > 0 {
t.Errorf("the following fields were exepcted, but missing from the result:\n%s", strings.Join(missing.List(), "\n"))
}
if len(unexpected) > 0 {
t.Errorf("the following fields were in the result, but unexpected:\n%s", strings.Join(unexpected.List(), "\n"))
}
}
}
var (
// KubeletConfiguration fields that contain file paths. If you update this, also update KubeletConfigurationPathRefs!
kubeletConfigurationPathFieldPaths = sets.NewString(
"PodManifestPath",
"Authentication.X509.ClientCAFile",
"TLSCertFile",
"TLSPrivateKeyFile",
"SeccompProfileRoot",
"ResolverConfig",
"VolumePluginDir",
"LockFilePath",
)
// KubeletConfiguration fields that do not contain file paths.
kubeletConfigurationNonPathFieldPaths = sets.NewString(
"Address",
"AllowPrivileged",
"Authentication.Anonymous.Enabled",
"Authentication.Webhook.CacheTTL.Duration",
"Authentication.Webhook.Enabled",
"Authorization.Mode",
"Authorization.Webhook.CacheAuthorizedTTL.Duration",
"Authorization.Webhook.CacheUnauthorizedTTL.Duration",
"CAdvisorPort",
"CPUCFSQuota",
"CPUManagerPolicy",
"CPUManagerReconcilePeriod.Duration",
"CgroupDriver",
"CgroupRoot",
"CgroupsPerQOS",
"ClusterDNS[*]",
"ClusterDomain",
"ConfigTrialDuration.Duration",
"ContentType",
"EnableContentionProfiling",
"EnableControllerAttachDetach",
"EnableDebuggingHandlers",
"EnableServer",
"EnforceNodeAllocatable[*]",
"EventBurst",
"EventRecordQPS",
"EvictionHard",
"EvictionMaxPodGracePeriod",
"EvictionMinimumReclaim",
"EvictionPressureTransitionPeriod.Duration",
"EvictionSoft",
"EvictionSoftGracePeriod",
"ExitOnLockContention",
"FailSwapOn",
"FeatureGates[*]",
"FileCheckFrequency.Duration",
"HTTPCheckFrequency.Duration",
"HairpinMode",
"HealthzBindAddress",
"HealthzPort",
"HostIPCSources[*]",
"HostNetworkSources[*]",
"HostPIDSources[*]",
"IPTablesDropBit",
"IPTablesMasqueradeBit",
"ImageGCHighThresholdPercent",
"ImageGCLowThresholdPercent",
"ImageMinimumGCAge.Duration",
"KubeAPIBurst",
"KubeAPIQPS",
"KubeReservedCgroup",
"KubeReserved[*]",
"KubeletCgroups",
"MakeIPTablesUtilChains",
"ManifestURL",
"ManifestURLHeader[*][*]",
"MaxOpenFiles",
"MaxPods",
"NodeLabels[*]",
"NodeStatusUpdateFrequency.Duration",
"OOMScoreAdj",
"PodCIDR",
"PodsPerCore",
"Port",
"ProtectKernelDefaults",
"ReadOnlyPort",
"RegisterNode",
"RegisterWithTaints[*].Effect",
"RegisterWithTaints[*].Key",
"RegisterWithTaints[*].TimeAdded.Time.ext",
"RegisterWithTaints[*].TimeAdded.Time.loc.cacheEnd",
"RegisterWithTaints[*].TimeAdded.Time.loc.cacheStart",
"RegisterWithTaints[*].TimeAdded.Time.loc.cacheZone.isDST",
"RegisterWithTaints[*].TimeAdded.Time.loc.cacheZone.name",
"RegisterWithTaints[*].TimeAdded.Time.loc.cacheZone.offset",
"RegisterWithTaints[*].TimeAdded.Time.loc.name",
"RegisterWithTaints[*].TimeAdded.Time.loc.tx[*].index",
"RegisterWithTaints[*].TimeAdded.Time.loc.tx[*].isstd",
"RegisterWithTaints[*].TimeAdded.Time.loc.tx[*].isutc",
"RegisterWithTaints[*].TimeAdded.Time.loc.tx[*].when",
"RegisterWithTaints[*].TimeAdded.Time.loc.zone[*].isDST",
"RegisterWithTaints[*].TimeAdded.Time.loc.zone[*].name",
"RegisterWithTaints[*].TimeAdded.Time.loc.zone[*].offset",
"RegisterWithTaints[*].TimeAdded.Time.wall",
"RegisterWithTaints[*].Value",
"RegistryBurst",
"RegistryPullQPS",
"RuntimeRequestTimeout.Duration",
"SerializeImagePulls",
"StreamingConnectionIdleTimeout.Duration",
"SyncFrequency.Duration",
"SystemCgroups",
"SystemReservedCgroup",
"SystemReserved[*]",
"TypeMeta.APIVersion",
"TypeMeta.Kind",
"VolumeStatsAggPeriod.Duration",
)
)
...@@ -44,7 +44,6 @@ go_test( ...@@ -44,7 +44,6 @@ go_test(
"//pkg/kubelet/kubeletconfig/util/files:go_default_library", "//pkg/kubelet/kubeletconfig/util/files:go_default_library",
"//pkg/kubelet/kubeletconfig/util/test:go_default_library", "//pkg/kubelet/kubeletconfig/util/test:go_default_library",
"//pkg/util/filesystem:go_default_library", "//pkg/util/filesystem:go_default_library",
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
], ],
) )
...@@ -72,5 +72,23 @@ func (loader *fsLoader) Load() (*kubeletconfig.KubeletConfiguration, error) { ...@@ -72,5 +72,23 @@ func (loader *fsLoader) Load() (*kubeletconfig.KubeletConfiguration, error) {
return nil, fmt.Errorf("init config file %q was empty, but some parameters are required", path) return nil, fmt.Errorf("init config file %q was empty, but some parameters are required", path)
} }
return utilcodec.DecodeKubeletConfiguration(loader.kubeletCodecs, data) kc, err := utilcodec.DecodeKubeletConfiguration(loader.kubeletCodecs, data)
if err != nil {
return nil, err
}
// make all paths absolute
resolveRelativePaths(kubeletconfig.KubeletConfigurationPathRefs(kc), loader.configDir)
return kc, nil
}
// resolveRelativePaths makes relative paths absolute by resolving them against `root`
func resolveRelativePaths(paths []*string, root string) {
for _, path := range paths {
// leave empty paths alone, "no path" is a valid input
// do not attempt to resolve paths that are already absolute
if len(*path) > 0 && !filepath.IsAbs(*path) {
*path = filepath.Join(root, *path)
}
}
} }
...@@ -19,11 +19,8 @@ package configfiles ...@@ -19,11 +19,8 @@ package configfiles
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/davecgh/go-spew/spew"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig" "k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig"
kubeletscheme "k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig/scheme" kubeletscheme "k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig/scheme"
...@@ -33,80 +30,184 @@ import ( ...@@ -33,80 +30,184 @@ import (
utilfs "k8s.io/kubernetes/pkg/util/filesystem" utilfs "k8s.io/kubernetes/pkg/util/filesystem"
) )
func addFile(fs utilfs.Filesystem, path string, file string) error { const configDir = "/test-config-dir"
if err := utilfiles.EnsureDir(fs, filepath.Dir(path)); err != nil { const relativePath = "relative/path/test"
return err
}
return utilfiles.ReplaceFile(fs, path, []byte(file))
}
func TestLoad(t *testing.T) { func TestLoad(t *testing.T) {
kubeletScheme, _, err := kubeletscheme.NewSchemeAndCodecs()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// get the built-in default configuration
external := &kubeletconfigv1alpha1.KubeletConfiguration{}
kubeletScheme.Default(external)
defaultConfig := &kubeletconfig.KubeletConfiguration{}
err = kubeletScheme.Convert(external, defaultConfig, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cases := []struct { cases := []struct {
desc string desc string
file string file *string
expect *kubeletconfig.KubeletConfiguration expect *kubeletconfig.KubeletConfiguration
err string err string
}{ }{
{"empty data", ``, nil, "was empty"}, // missing file
{
"missing file",
nil,
nil,
"failed to read",
},
// empty file
{
"empty file",
newString(``),
nil,
"was empty",
},
// invalid format // invalid format
{"invalid yaml", `*`, nil, "failed to decode"}, {
{"invalid json", `{*`, nil, "failed to decode"}, "invalid yaml",
newString(`*`),
nil,
"failed to decode",
},
{
"invalid json",
newString(`{*`),
nil,
"failed to decode",
},
// invalid object // invalid object
{"missing kind", `{"apiVersion":"kubeletconfig/v1alpha1"}`, nil, "failed to decode"}, {
{"missing version", `{"kind":"KubeletConfiguration"}`, nil, "failed to decode"}, "missing kind",
{"unregistered kind", `{"kind":"BogusKind","apiVersion":"kubeletconfig/v1alpha1"}`, nil, "failed to decode"}, newString(`{"apiVersion":"kubeletconfig/v1alpha1"}`),
{"unregistered version", `{"kind":"KubeletConfiguration","apiVersion":"bogusversion"}`, nil, "failed to decode"}, nil,
"failed to decode",
},
{
"missing version",
newString(`{"kind":"KubeletConfiguration"}`),
nil,
"failed to decode",
},
{
"unregistered kind",
newString(`{"kind":"BogusKind","apiVersion":"kubeletconfig/v1alpha1"}`),
nil,
"failed to decode",
},
{
"unregistered version",
newString(`{"kind":"KubeletConfiguration","apiVersion":"bogusversion"}`),
nil,
"failed to decode",
},
// empty object with correct kind and version should result in the defaults for that kind and version // empty object with correct kind and version should result in the defaults for that kind and version
{"default from yaml", `kind: KubeletConfiguration {
apiVersion: kubeletconfig/v1alpha1`, defaultConfig, ""}, "default from yaml",
{"default from json", `{"kind":"KubeletConfiguration","apiVersion":"kubeletconfig/v1alpha1"}`, defaultConfig, ""}, newString(`kind: KubeletConfiguration
apiVersion: kubeletconfig/v1alpha1`),
newConfig(t),
"",
},
{
"default from json",
newString(`{"kind":"KubeletConfiguration","apiVersion":"kubeletconfig/v1alpha1"}`),
newConfig(t),
"",
},
// relative path
{
"yaml, relative path is resolved",
newString(fmt.Sprintf(`kind: KubeletConfiguration
apiVersion: kubeletconfig/v1alpha1
podManifestPath: %s`, relativePath)),
func() *kubeletconfig.KubeletConfiguration {
kc := newConfig(t)
kc.PodManifestPath = filepath.Join(configDir, relativePath)
return kc
}(),
"",
},
{
"json, relative path is resolved",
newString(fmt.Sprintf(`{"kind":"KubeletConfiguration","apiVersion":"kubeletconfig/v1alpha1","podManifestPath":"%s"}`, relativePath)),
func() *kubeletconfig.KubeletConfiguration {
kc := newConfig(t)
kc.PodManifestPath = filepath.Join(configDir, relativePath)
return kc
}(),
"",
},
} }
fs := utilfs.NewFakeFs() for _, c := range cases {
for i := range cases { t.Run(c.desc, func(t *testing.T) {
dir := fmt.Sprintf("/%d", i) fs := utilfs.NewFakeFs()
if err := addFile(fs, filepath.Join(dir, kubeletFile), cases[i].file); err != nil { if c.file != nil {
t.Fatalf("unexpected error: %v", err) if err := addFile(fs, filepath.Join(configDir, kubeletFile), *c.file); err != nil {
} t.Fatalf("unexpected error: %v", err)
loader, err := NewFsLoader(fs, dir) }
if err != nil { }
t.Fatalf("unexpected error: %v", err) loader, err := NewFsLoader(fs, configDir)
} if err != nil {
kc, err := loader.Load() t.Fatalf("unexpected error: %v", err)
if utiltest.SkipRest(t, cases[i].desc, err, cases[i].err) { }
continue kc, err := loader.Load()
} if utiltest.SkipRest(t, c.desc, err, c.err) {
// we expect the parsed configuration to match what we described in the ConfigMap return
if !apiequality.Semantic.DeepEqual(cases[i].expect, kc) { }
t.Errorf("case %q, expect config %s but got %s", cases[i].desc, spew.Sdump(cases[i].expect), spew.Sdump(kc)) if !apiequality.Semantic.DeepEqual(c.expect, kc) {
} t.Fatalf("expect %#v but got %#v", *c.expect, *kc)
}
})
} }
}
// finally test for a missing file func TestResolveRelativePaths(t *testing.T) {
desc := "missing kubelet file" absolutePath := filepath.Join(configDir, "absolute")
contains := "failed to read" cases := []struct {
loader, err := NewFsLoader(fs, "/fake") desc string
path string
expect string
}{
{"empty path", "", ""},
{"absolute path", absolutePath, absolutePath},
{"relative path", relativePath, filepath.Join(configDir, relativePath)},
}
paths := kubeletconfig.KubeletConfigurationPathRefs(newConfig(t))
if len(paths) == 0 {
t.Fatalf("requires at least one path field to exist in the KubeletConfiguration type")
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
// set the path, resolve it, and check if it resolved as we would expect
*(paths[0]) = c.path
resolveRelativePaths(paths, configDir)
if *(paths[0]) != c.expect {
t.Fatalf("expect %s but got %s", c.expect, *(paths[0]))
}
})
}
}
func newString(s string) *string {
return &s
}
func addFile(fs utilfs.Filesystem, path string, file string) error {
if err := utilfiles.EnsureDir(fs, filepath.Dir(path)); err != nil {
return err
}
return utilfiles.ReplaceFile(fs, path, []byte(file))
}
func newConfig(t *testing.T) *kubeletconfig.KubeletConfiguration {
kubeletScheme, _, err := kubeletscheme.NewSchemeAndCodecs()
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
_, err = loader.Load() // get the built-in default configuration
if err == nil { external := &kubeletconfigv1alpha1.KubeletConfiguration{}
t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains) kubeletScheme.Default(external)
} else if !strings.Contains(err.Error(), contains) { kc := &kubeletconfig.KubeletConfiguration{}
t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error()) err = kubeletScheme.Convert(external, kc, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
} }
return kc
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment