diff --git a/pkg/api/types.go b/pkg/api/types.go index 5af815c162d16..1ec48d48695a4 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -230,7 +230,10 @@ type GitRepo struct { // TODO: Consider credentials here. } -// Adapts a Secret into a VolumeSource +// Adapts a Secret into a VolumeSource. +// +// The contents of the target Secret's Data field will be presented in a volume +// as files using the keys in the Data field as the file names. type SecretSource struct { // Reference to a Secret Target ObjectReference `json:"target"` @@ -1318,15 +1321,22 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items"` } -// Secret holds secret data of a certain type +// Secret holds secret data of a certain type. The total bytes of the values in +// the Data field must be less than MaxSecretSize bytes. type Secret struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty"` + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // The serialized form of the secret data is a base64 encoded string. Data map[string][]byte `json:"data,omitempty"` - Type SecretType `json:"type,omitempty"` + + // Used to facilitate programatic handling of secret data. + Type SecretType `json:"type,omitempty"` } +const MaxSecretSize = 1 * 1024 * 1024 + type SecretType string const ( @@ -1339,5 +1349,3 @@ type SecretList struct { Items []Secret `json:"items"` } - -const MaxSecretSize = 1 * 1024 * 1024 diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index bd7b69e4d2938..afe92e2df0827 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -1100,13 +1100,21 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items"` } +// Secret holds secret data of a certain type. The total bytes of the values in +// the Data field must be less than MaxSecretSize bytes. type Secret struct { TypeMeta `json:",inline"` + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // The serialized form of the secret data is a base64 encoded string. Data map[string][]byte `json:"data,omitempty"` - Type SecretType `json:"type,omitempty"` + + // Used to facilitate programatic handling of secret data. + Type SecretType `json:"type,omitempty"` } +const MaxSecretSize = 1 * 1024 * 1024 + type SecretType string const ( diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index c76f148f5dc2a..6002bcc2d0b85 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -1103,14 +1103,21 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items"` } -// Secret holds secret data of a certain type +// Secret holds secret data of a certain type. The total bytes of the values in +// the Data field must be less than MaxSecretSize bytes. type Secret struct { TypeMeta `json:",inline"` + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. Data map[string][]byte `json:"data,omitempty"` - Type SecretType `json:"type,omitempty"` + + // Used to facilitate programatic handling of secret data. + // The serialized form of the secret data is a base64 encoded string. + Type SecretType `json:"type,omitempty"` } +const MaxSecretSize = 1 * 1024 * 1024 + type SecretType string const ( diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index b7bb118844dda..3b1214fde310d 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1243,16 +1243,22 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items"` } -// Secret holds mappings between paths and secret data -// TODO: shouldn't "Secret" be a plural? +// Secret holds secret data of a certain type. The total bytes of the values in +// the Data field must be less than MaxSecretSize bytes. type Secret struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty"` + // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. + // The serialized form of the secret data is a base64 encoded string. Data map[string][]byte `json:"data,omitempty"` - Type SecretType `json:"type,omitempty"` + + // Used to facilitate programatic handling of secret data. + Type SecretType `json:"type,omitempty"` } +const MaxSecretSize = 1 * 1024 * 1024 + type SecretType string const ( diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index e12b2a1e4b59b..619d6cde9bf6a 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -853,9 +853,14 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { } totalSize := 0 - for _, value := range secret.Data { + for key, value := range secret.Data { + if !util.IsDNSSubdomain(key) { + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("data[%v]", key), key, cIdentifierErrorMsg)) + } + totalSize += len(value) } + if totalSize > api.MaxSecretSize { allErrs = append(allErrs, errs.NewFieldForbidden("data", "Maximum secret size exceeded")) } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 5d28c23fab384..9e7e3c7647cb8 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2497,7 +2497,7 @@ func TestValidateSecret(t *testing.T) { return api.Secret{ ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"}, Data: map[string][]byte{ - "foo": []byte("bar"), + "data-1": []byte("bar"), }, } } @@ -2508,6 +2508,7 @@ func TestValidateSecret(t *testing.T) { emptyNs = validSecret() invalidNs = validSecret() overMaxSize = validSecret() + invalidKey = validSecret() ) emptyName.Name = "" @@ -2517,6 +2518,7 @@ func TestValidateSecret(t *testing.T) { overMaxSize.Data = map[string][]byte{ "over": make([]byte, api.MaxSecretSize+1), } + invalidKey.Data["a..b"] = []byte("whoops") tests := map[string]struct { secret api.Secret @@ -2528,6 +2530,7 @@ func TestValidateSecret(t *testing.T) { "empty namespace": {emptyNs, false}, "invalid namespace": {invalidNs, false}, "over max size": {overMaxSize, false}, + "invalid key": {invalidKey, false}, } for name, tc := range tests { diff --git a/pkg/kubelet/server/plugins.go b/pkg/kubelet/server/plugins.go index 7b79851de7b7a..ceb573dce6418 100644 --- a/pkg/kubelet/server/plugins.go +++ b/pkg/kubelet/server/plugins.go @@ -26,6 +26,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/gce_pd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/git_repo" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/host_path" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/secret" ) // ProbeVolumePlugins collects all volume plugins into an easy to use list. @@ -39,6 +40,7 @@ func ProbeVolumePlugins() []volume.Plugin { allPlugins = append(allPlugins, gce_pd.ProbeVolumePlugins()...) allPlugins = append(allPlugins, git_repo.ProbeVolumePlugins()...) allPlugins = append(allPlugins, host_path.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, secret.ProbeVolumePlugins()...) return allPlugins } diff --git a/pkg/kubelet/server/server.go b/pkg/kubelet/server/server.go index 13860d6ad8e18..727b003b950b8 100644 --- a/pkg/kubelet/server/server.go +++ b/pkg/kubelet/server/server.go @@ -179,6 +179,8 @@ func (s *KubeletServer) Run(_ []string) error { glog.Warningf("No API client: %v", err) } + glog.Infof("Using root directory: %v", s.RootDirectory) + credentialprovider.SetPreferredDockercfgPath(s.RootDirectory) kcfg := KubeletConfig{ diff --git a/pkg/kubelet/volume/empty_dir/empty_dir_test.go b/pkg/kubelet/volume/empty_dir/empty_dir_test.go index addeb013a35a4..33c0bad334d13 100644 --- a/pkg/kubelet/volume/empty_dir/empty_dir_test.go +++ b/pkg/kubelet/volume/empty_dir/empty_dir_test.go @@ -27,7 +27,7 @@ import ( func TestCanSupport(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir") if err != nil { @@ -46,7 +46,7 @@ func TestCanSupport(t *testing.T) { func TestPlugin(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir") if err != nil { @@ -100,7 +100,7 @@ func TestPlugin(t *testing.T) { func TestPluginBackCompat(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/empty-dir") if err != nil { @@ -125,7 +125,7 @@ func TestPluginBackCompat(t *testing.T) { func TestPluginLegacy(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("empty") if err != nil { diff --git a/pkg/kubelet/volume/gce_pd/gce_pd_test.go b/pkg/kubelet/volume/gce_pd/gce_pd_test.go index 01129bd5fab9e..13f08563d36cc 100644 --- a/pkg/kubelet/volume/gce_pd/gce_pd_test.go +++ b/pkg/kubelet/volume/gce_pd/gce_pd_test.go @@ -28,7 +28,7 @@ import ( func TestCanSupport(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/gce-pd") if err != nil { @@ -80,7 +80,7 @@ func (fake *fakeMounter) List() ([]mount.MountPoint, error) { func TestPlugin(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/gce-pd") if err != nil { @@ -146,7 +146,7 @@ func TestPlugin(t *testing.T) { func TestPluginLegacy(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"/tmp/fake", nil}) plug, err := plugMgr.FindPluginByName("gce-pd") if err != nil { diff --git a/pkg/kubelet/volume/git_repo/git_repo_test.go b/pkg/kubelet/volume/git_repo/git_repo_test.go index f4443bcf9cef9..5e3632fd3b8f5 100644 --- a/pkg/kubelet/volume/git_repo/git_repo_test.go +++ b/pkg/kubelet/volume/git_repo/git_repo_test.go @@ -35,7 +35,7 @@ func newTestHost(t *testing.T) volume.Host { if err != nil { t.Fatalf("can't make a temp rootdir: %v", err) } - return &volume.FakeHost{tempDir} + return &volume.FakeHost{tempDir, nil} } func TestCanSupport(t *testing.T) { diff --git a/pkg/kubelet/volume/host_path/host_path_test.go b/pkg/kubelet/volume/host_path/host_path_test.go index 2914fd46cf523..d19d4386f225b 100644 --- a/pkg/kubelet/volume/host_path/host_path_test.go +++ b/pkg/kubelet/volume/host_path/host_path_test.go @@ -26,7 +26,7 @@ import ( func TestCanSupport(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/host-path") if err != nil { @@ -45,7 +45,7 @@ func TestCanSupport(t *testing.T) { func TestPlugin(t *testing.T) { plugMgr := volume.PluginMgr{} - plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"fake"}) + plugMgr.InitPlugins(ProbeVolumePlugins(), &volume.FakeHost{"fake", nil}) plug, err := plugMgr.FindPluginByName("kubernetes.io/host-path") if err != nil { diff --git a/pkg/kubelet/volume/plugins.go b/pkg/kubelet/volume/plugins.go index 5180846698a0c..9c15986202464 100644 --- a/pkg/kubelet/volume/plugins.go +++ b/pkg/kubelet/volume/plugins.go @@ -22,6 +22,7 @@ import ( "sync" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" @@ -76,6 +77,9 @@ type Host interface { // does not exist, the result of this call might not exist. This // directory might not actually exist on disk yet. GetPodPluginDir(podUID types.UID, pluginName string) string + + // GetKubeClient returns a client interface + GetKubeClient() client.Interface } // PluginMgr tracks registered plugins. diff --git a/pkg/kubelet/volume/secret/secret.go b/pkg/kubelet/volume/secret/secret.go new file mode 100644 index 0000000000000..1db22edb93822 --- /dev/null +++ b/pkg/kubelet/volume/secret/secret.go @@ -0,0 +1,133 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +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 secret + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" + "github.com/golang/glog" +) + +// ProbeVolumePlugin is the entry point for plugin detection in a package. +func ProbeVolumePlugins() []volume.Plugin { + return []volume.Plugin{&secretPlugin{}} +} + +const ( + secretPluginName = "kubernetes.io/secret" +) + +// secretPlugin implements the VolumePlugin interface. +type secretPlugin struct { + host volume.Host +} + +func (plugin *secretPlugin) Init(host volume.Host) { + plugin.host = host +} + +func (plugin *secretPlugin) Name() string { + return secretPluginName +} + +func (plugin *secretPlugin) CanSupport(spec *api.Volume) bool { + if spec.Source.Secret != nil { + return true + } + + return false +} + +func (plugin *secretPlugin) NewBuilder(spec *api.Volume, podUID types.UID) (volume.Builder, error) { + return plugin.newBuilderInternal(spec, podUID) +} + +func (plugin *secretPlugin) newBuilderInternal(spec *api.Volume, podUID types.UID) (volume.Builder, error) { + return &secretVolume{spec.Name, podUID, plugin, &spec.Source.Secret.Target}, nil +} + +func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { + return plugin.newCleanerInternal(volName, podUID) +} + +func (plugin *secretPlugin) newCleanerInternal(volName string, podUID types.UID) (volume.Cleaner, error) { + return &secretVolume{volName, podUID, plugin, nil}, nil +} + +// secretVolume handles retrieving secrets from the API server +// and placing them into the volume on the host. +type secretVolume struct { + volName string + podUID types.UID + plugin *secretPlugin + secretRef *api.ObjectReference +} + +func (sv *secretVolume) SetUp() error { + // TODO: explore tmpfs for secret volumes + hostPath := sv.GetPath() + glog.V(3).Infof("Setting up volume %v for pod %v at %v", sv.volName, sv.podUID, hostPath) + err := os.MkdirAll(hostPath, 0777) + if err != nil { + return err + } + + kubeClient := sv.plugin.host.GetKubeClient() + if kubeClient == nil { + return fmt.Errorf("Cannot setup secret volume %v because kube client is not configured", sv) + } + + secret, err := kubeClient.Secrets(sv.secretRef.Namespace).Get(sv.secretRef.Name) + if err != nil { + glog.Errorf("Couldn't get secret %v/%v", sv.secretRef.Namespace, sv.secretRef.Name) + return err + } + + for name, data := range secret.Data { + hostFilePath := path.Join(hostPath, name) + err := ioutil.WriteFile(hostFilePath, data, 0777) + if err != nil { + glog.Errorf("Error writing secret data to host path: %v, %v", hostFilePath, err) + return err + } + } + + return nil +} + +func (sv *secretVolume) GetPath() string { + return sv.plugin.host.GetPodVolumeDir(sv.podUID, volume.EscapePluginName(secretPluginName), sv.volName) +} + +func (sv *secretVolume) TearDown() error { + glog.V(3).Infof("Tearing down volume %v for pod %v at %v", sv.volName, sv.podUID, sv.GetPath()) + tmpDir, err := volume.RenameDirectory(sv.GetPath(), sv.volName+".deleting~") + if err != nil { + return err + } + err = os.RemoveAll(tmpDir) + if err != nil { + return err + } + return nil +} diff --git a/pkg/kubelet/volume/secret/secret_test.go b/pkg/kubelet/volume/secret/secret_test.go new file mode 100644 index 0000000000000..d208795db94f6 --- /dev/null +++ b/pkg/kubelet/volume/secret/secret_test.go @@ -0,0 +1,160 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +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 secret + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" +) + +func newTestHost(t *testing.T, fakeKubeClient client.Interface) volume.Host { + tempDir, err := ioutil.TempDir("/tmp", "secret_volume_test.") + if err != nil { + t.Fatalf("can't make a temp rootdir: %v", err) + } + + return &volume.FakeHost{tempDir, fakeKubeClient} +} + +func TestCanSupport(t *testing.T) { + pluginMgr := volume.PluginMgr{} + pluginMgr.InitPlugins(ProbeVolumePlugins(), newTestHost(t, nil)) + + plugin, err := pluginMgr.FindPluginByName(secretPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if plugin.Name() != secretPluginName { + t.Errorf("Wrong name: %s", plugin.Name()) + } + if !plugin.CanSupport(&api.Volume{Source: api.VolumeSource{Secret: &api.SecretSource{Target: api.ObjectReference{}}}}) { + t.Errorf("Expected true") + } +} + +func TestPlugin(t *testing.T) { + var ( + testPodUID = "test_pod_uid" + testVolumeName = "test_volume_name" + testNamespace = "test_secret_namespace" + testName = "test_secret_name" + ) + + volumeSpec := &api.Volume{ + Name: testVolumeName, + Source: api.VolumeSource{ + Secret: &api.SecretSource{ + Target: api.ObjectReference{ + Namespace: testNamespace, + Name: testName, + }, + }, + }, + } + + secret := api.Secret{ + ObjectMeta: api.ObjectMeta{ + Namespace: testNamespace, + Name: testName, + }, + Data: map[string][]byte{ + "data-1": []byte("value-1"), + "data-2": []byte("value-2"), + "data-3": []byte("value-3"), + }, + } + + client := &client.Fake{ + Secret: secret, + } + + pluginMgr := volume.PluginMgr{} + pluginMgr.InitPlugins(ProbeVolumePlugins(), newTestHost(t, client)) + + plugin, err := pluginMgr.FindPluginByName(secretPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + + builder, err := plugin.NewBuilder(volumeSpec, types.UID(testPodUID)) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + if builder == nil { + t.Errorf("Got a nil Builder: %v") + } + + volumePath := builder.GetPath() + if !strings.HasSuffix(volumePath, fmt.Sprintf("pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name")) { + t.Errorf("Got unexpected path: %s", volumePath) + } + + err = builder.SetUp() + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + if _, err := os.Stat(volumePath); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volumePath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + + for key, value := range secret.Data { + secretDataHostPath := path.Join(volumePath, key) + if _, err := os.Stat(secretDataHostPath); err != nil { + t.Fatalf("SetUp() failed, couldn't find secret data on disk: %v", secretDataHostPath) + } else { + actualSecretBytes, err := ioutil.ReadFile(secretDataHostPath) + if err != nil { + t.Fatalf("Couldn't read secret data from: %v", secretDataHostPath) + } + + actualSecretValue := string(actualSecretBytes) + if string(value) != actualSecretValue { + t.Errorf("Unexpected value; expected %q, got %q", value, actualSecretValue) + } + } + } + + cleaner, err := plugin.NewCleaner(testVolumeName, types.UID(testPodUID)) + if err != nil { + t.Errorf("Failed to make a new Cleaner: %v", err) + } + if cleaner == nil { + t.Errorf("Got a nil Cleaner: %v") + } + + if err := cleaner.TearDown(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(volumePath); err == nil { + t.Errorf("TearDown() failed, volume path still exists: %s", volumePath) + } else if !os.IsNotExist(err) { + t.Errorf("SetUp() failed: %v", err) + } +} diff --git a/pkg/kubelet/volume/testing.go b/pkg/kubelet/volume/testing.go index 68dee5dffbb4e..84aa4724648b7 100644 --- a/pkg/kubelet/volume/testing.go +++ b/pkg/kubelet/volume/testing.go @@ -21,12 +21,14 @@ import ( "path" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" ) // FakeHost is useful for testing volume plugins. type FakeHost struct { - RootDir string + RootDir string + KubeClient client.Interface } func (f *FakeHost) GetPluginDir(podUID string) string { @@ -41,6 +43,10 @@ func (f *FakeHost) GetPodPluginDir(podUID types.UID, pluginName string) string { return path.Join(f.RootDir, "pods", string(podUID), "plugins", pluginName) } +func (f *FakeHost) GetKubeClient() client.Interface { + return f.KubeClient +} + // FakePlugin is useful for for testing. It tries to be a fully compliant // plugin, but all it does is make empty directories. // Use as: diff --git a/pkg/kubelet/volumes.go b/pkg/kubelet/volumes.go index 2f49c007cd81c..461e37c60ce32 100644 --- a/pkg/kubelet/volumes.go +++ b/pkg/kubelet/volumes.go @@ -22,6 +22,7 @@ import ( "path" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/davecgh/go-spew/spew" @@ -48,6 +49,10 @@ func (vh *volumeHost) GetPodPluginDir(podUID types.UID, pluginName string) strin return vh.kubelet.getPodPluginDir(podUID, pluginName) } +func (vh *volumeHost) GetKubeClient() client.Interface { + return vh.kubelet.kubeClient +} + func (kl *Kubelet) newVolumeBuilderFromPlugins(spec *api.Volume, podUID types.UID) volume.Builder { plugin, err := kl.volumePluginMgr.FindPluginBySpec(spec) if err != nil { diff --git a/test/e2e/secrets.go b/test/e2e/secrets.go new file mode 100644 index 0000000000000..cd1990bf693bc --- /dev/null +++ b/test/e2e/secrets.go @@ -0,0 +1,166 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 e2e + +import ( + "fmt" + "strings" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Secrets", func() { + var c *client.Client + + BeforeEach(func() { + var err error + c, err = loadClient() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be consumable from pods", func() { + ns := api.NamespaceDefault + name := "secret-test-" + string(util.NewUUID()) + volumeName := "secret-volume" + volumeMountPath := "/etc/secret-volume" + + secret := &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Namespace: ns, + Name: name, + }, + Data: map[string][]byte{ + "data-1": []byte("value-1\n"), + "data-2": []byte("value-2\n"), + "data-3": []byte("value-3\n"), + }, + } + + secret, err := c.Secrets(ns).Create(secret) + By(fmt.Sprintf("Creating secret with name %s", secret.Name)) + if err != nil { + Fail(fmt.Sprintf("unable to create test secret %s: %v", secret.Name, err)) + } + + // Clean up secret + defer func() { + By("Cleaning up the secret") + if err = c.Secrets(ns).Delete(secret.Name); err != nil { + Fail(fmt.Sprintf("unable to delete secret %v: %v", secret.Name, err)) + } + }() + + By(fmt.Sprintf("Creating a pod to consume secret %v", secret.Name)) + // Make a client pod that verifies that it has the service environment variables. + clientName := "client-secrets-" + string(util.NewUUID()) + clientPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: clientName, + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: volumeName, + Source: api.VolumeSource{ + Secret: &api.SecretSource{ + Target: api.ObjectReference{ + Kind: "Secret", + Namespace: ns, + Name: name, + }, + }, + }, + }, + }, + Containers: []api.Container{ + { + Name: "catcont", + Image: "busybox", + Command: []string{"sh", "-c", "cat /etc/secret-volume/data-1; sleep 600"}, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: api.RestartPolicy{ + Never: &api.RestartPolicyNever{}, + }, + }, + } + + _, err = c.Pods(ns).Create(clientPod) + if err != nil { + Fail(fmt.Sprintf("Failed to create pod: %v", err)) + } + defer func() { + c.Pods(ns).Delete(clientPod.Name) + }() + + // Wait for client pod to complete. + expectNoError(waitForPodRunning(c, clientPod.Name, 60*time.Second)) + + // Grab its logs. Get host first. + clientPodStatus, err := c.Pods(ns).Get(clientPod.Name) + if err != nil { + Fail(fmt.Sprintf("Failed to get clientPod to know host: %v", err)) + } + By(fmt.Sprintf("Trying to get logs from host %s pod %s container %s: %v", + clientPodStatus.Status.Host, clientPodStatus.Name, clientPodStatus.Spec.Containers[0].Name, err)) + var logs []byte + start := time.Now() + + // Sometimes the actual containers take a second to get started, try to get logs for 60s + for time.Now().Sub(start) < (60 * time.Second) { + logs, err = c.Get(). + Prefix("proxy"). + Resource("minions"). + Name(clientPodStatus.Status.Host). + Suffix("containerLogs", ns, clientPodStatus.Name, clientPodStatus.Spec.Containers[0].Name). + Do(). + Raw() + fmt.Sprintf("clientPod logs:%v\n", string(logs)) + By(fmt.Sprintf("clientPod logs:%v\n", string(logs))) + if strings.Contains(string(logs), "Internal Error") { + By(fmt.Sprintf("Failed to get logs from host %s pod %s container %s: %v", + clientPodStatus.Status.Host, clientPodStatus.Name, clientPodStatus.Spec.Containers[0].Name, string(logs))) + time.Sleep(5 * time.Second) + continue + } + break + } + + toFind := []string{ + "value-1", + } + + for _, m := range toFind { + Expect(string(logs)).To(ContainSubstring(m), "%q in secret data", m) + } + + // We could try a wget the service from the client pod. But services.sh e2e test covers that pretty well. + }) +})