/* 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 store import ( "fmt" "path/filepath" "testing" "time" "github.com/davecgh/go-spew/spew" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/kubelet/kubeletconfig/checkpoint" utilfiles "k8s.io/kubernetes/pkg/kubelet/kubeletconfig/util/files" utiltest "k8s.io/kubernetes/pkg/kubelet/kubeletconfig/util/test" utilfs "k8s.io/kubernetes/pkg/util/filesystem" ) const testCheckpointsDir = "/test-checkpoints-dir" func newInitializedFakeFsStore() (*fsStore, error) { fs := utilfs.NewFakeFs() store := NewFsStore(fs, testCheckpointsDir) if err := store.Initialize(); err != nil { return nil, err } return store.(*fsStore), nil } func TestFsStoreInitialize(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("fsStore.Initialize() failed with error: %v", err) } // check that testCheckpointsDir exists _, err = store.fs.Stat(testCheckpointsDir) if err != nil { t.Fatalf("expect %q to exist, but stat failed with error: %v", testCheckpointsDir, err) } // check that testCheckpointsDir contains the curFile curPath := filepath.Join(testCheckpointsDir, curFile) _, err = store.fs.Stat(curPath) if err != nil { t.Fatalf("expect %q to exist, but stat failed with error: %v", curPath, err) } // check that testCheckpointsDir contains the lkgFile lkgPath := filepath.Join(testCheckpointsDir, lkgFile) _, err = store.fs.Stat(lkgPath) if err != nil { t.Fatalf("expect %q to exist, but stat failed with error: %v", lkgPath, err) } } func TestFsStoreExists(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } // create a checkpoint file; this is enough for an exists check cpt, err := checkpoint.NewConfigMapCheckpoint(&apiv1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{UID: "uid"}, }) if err != nil { t.Fatalf("could not construct checkpoint, error: %v", err) } saveTestCheckpointFile(t, store.fs, cpt) cases := []struct { desc string uid string // the uid to test expect bool err string }{ {"exists", "uid", true, ""}, {"does not exist", "bogus-uid", false, ""}, } for _, c := range cases { ok, err := store.Exists(c.uid) if utiltest.SkipRest(t, c.desc, err, c.err) { continue } if c.expect != ok { t.Errorf("case %q, expect %t but got %t", c.desc, c.expect, ok) } } } func TestFsStoreSave(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } cpt, err := checkpoint.NewConfigMapCheckpoint(&apiv1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{UID: "uid"}, }) if err != nil { t.Fatalf("could not construct checkpoint, error: %v", err) } // save the checkpoint err = store.Save(cpt) if err != nil { t.Fatalf("unable to save checkpoint, error: %v", err) } // expect the saved checkpoint file to match the encoding of the checkpoint data, err := cpt.Encode() if err != nil { t.Fatalf("unable to encode the checkpoint, error: %v", err) } expect := string(data) data = readTestCheckpointFile(t, store.fs, cpt.UID()) cptFile := string(data) if expect != cptFile { t.Errorf("expect %q but got %q", expect, cptFile) } } func TestFsStoreLoad(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } const uid = "uid" cpt, err := checkpoint.NewConfigMapCheckpoint(&apiv1.ConfigMap{ObjectMeta: metav1.ObjectMeta{UID: types.UID(uid)}}) if err != nil { t.Fatalf("unable to construct checkpoint, error: %v", err) } cases := []struct { desc string loadUID string cpt checkpoint.Checkpoint err string }{ {"checkpoint exists", uid, cpt, ""}, {"checkpoint does not exist", "bogus-uid", nil, "failed to read"}, } for _, c := range cases { if c.cpt != nil { saveTestCheckpointFile(t, store.fs, c.cpt) } cpt, err := store.Load(c.loadUID) if utiltest.SkipRest(t, c.desc, err, c.err) { continue } if !checkpoint.EqualCheckpoints(c.cpt, cpt) { t.Errorf("case %q, expect %q but got %q", c.desc, spew.Sdump(c.cpt), spew.Sdump(cpt)) } } } func TestFsStoreRoundTrip(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } const uid = "uid" cpt, err := checkpoint.NewConfigMapCheckpoint(&apiv1.ConfigMap{ObjectMeta: metav1.ObjectMeta{UID: types.UID(uid)}}) if err != nil { t.Fatalf("unable to construct checkpoint, error: %v", err) } err = store.Save(cpt) if err != nil { t.Fatalf("unable to save checkpoint, error: %v", err) } cptAfter, err := store.Load(uid) if err != nil { t.Fatalf("unable to load checkpoint, error: %v", err) } if !checkpoint.EqualCheckpoints(cpt, cptAfter) { t.Errorf("expect %q but got %q", spew.Sdump(cpt), spew.Sdump(cptAfter)) } } func TestFsStoreCurrentModified(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } // create an empty current file, this is good enough for testing saveTestSourceFile(t, store.fs, curFile, nil) // set the timestamps to the current time, so we can compare to result of store.SetCurrentModified now := time.Now() err = store.fs.Chtimes(filepath.Join(testCheckpointsDir, curFile), now, now) if err != nil { t.Fatalf("could not change timestamps, error: %v", err) } // for now we hope that the system won't truncate the time to a less precise unit, // if this test fails on certain systems that may be the reason. modTime, err := store.CurrentModified() if err != nil { t.Fatalf("unable to determine modification time of current config source, error: %v", err) } if !now.Equal(modTime) { t.Errorf("expect %q but got %q", now.Format(time.RFC3339), modTime.Format(time.RFC3339)) } } func TestFsStoreCurrent(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ ConfigMapRef: &apiv1.ObjectReference{Name: "name", Namespace: "namespace", UID: "uid"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } cases := []struct { desc string expect checkpoint.RemoteConfigSource err string }{ {"default source", nil, ""}, {"non-default source", source, ""}, } for _, c := range cases { // save the last known good source saveTestSourceFile(t, store.fs, curFile, c.expect) // load last-known-good and compare to expected result source, err := store.Current() if utiltest.SkipRest(t, c.desc, err, c.err) { continue } if !checkpoint.EqualRemoteConfigSources(c.expect, source) { t.Errorf("case %q, expect %q but got %q", spew.Sdump(c.expect), spew.Sdump(c.expect), spew.Sdump(source)) } } } func TestFsStoreLastKnownGood(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ ConfigMapRef: &apiv1.ObjectReference{Name: "name", Namespace: "namespace", UID: "uid"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } cases := []struct { desc string expect checkpoint.RemoteConfigSource err string }{ {"default source", nil, ""}, {"non-default source", source, ""}, } for _, c := range cases { // save the last known good source saveTestSourceFile(t, store.fs, lkgFile, c.expect) // load last-known-good and compare to expected result source, err := store.LastKnownGood() if utiltest.SkipRest(t, c.desc, err, c.err) { continue } if !checkpoint.EqualRemoteConfigSources(c.expect, source) { t.Errorf("case %q, expect %q but got %q", spew.Sdump(c.expect), spew.Sdump(c.expect), spew.Sdump(source)) } } } func TestFsStoreSetCurrent(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } const uid = "uid" expect := fmt.Sprintf(`{"kind":"NodeConfigSource","apiVersion":"v1","configMapRef":{"namespace":"namespace","name":"name","uid":"%s"}}%s`, uid, "\n") source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{ Name: "name", Namespace: "namespace", UID: types.UID(uid)}}) if err != nil { t.Fatalf("unexpected error: %v", err) } // save the current source if err := store.SetCurrent(source); err != nil { t.Fatalf("unexpected error: %v", err) } // check that the source saved as we would expect data := readTestSourceFile(t, store.fs, curFile) if expect != string(data) { t.Errorf("expect current source file to contain %q, but got %q", expect, string(data)) } } func TestFsStoreSetCurrentUpdated(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } cases := []struct { current string newCurrent string expectUpdated bool err string }{ {"", "", false, ""}, {"uid", "", true, ""}, {"", "uid", true, ""}, {"uid", "uid", false, ""}, {"uid", "other-uid", true, ""}, {"other-uid", "uid", true, ""}, {"other-uid", "other-uid", false, ""}, } for _, c := range cases { // construct current source var source checkpoint.RemoteConfigSource expectSource := "" if len(c.current) > 0 { expectSource = fmt.Sprintf(`{"kind":"NodeConfigSource","apiVersion":"v1","configMapRef":{"namespace":"namespace","name":"name","uid":"%s"}}%s`, c.current, "\n") source, _, err = checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{ Name: "name", Namespace: "namespace", UID: types.UID(c.current)}}) if err != nil { t.Fatalf("unexpected error: %v", err) } } // construct new source var newSource checkpoint.RemoteConfigSource expectNewSource := "" if len(c.newCurrent) > 0 { expectNewSource = fmt.Sprintf(`{"kind":"NodeConfigSource","apiVersion":"v1","configMapRef":{"namespace":"namespace","name":"new-name","uid":"%s"}}%s`, c.newCurrent, "\n") newSource, _, err = checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{ Name: "new-name", Namespace: "namespace", UID: types.UID(c.newCurrent)}}) if err != nil { t.Fatalf("unexpected error: %v", err) } } // set the initial current if err := store.SetCurrent(source); err != nil { t.Fatalf("unexpected error: %v", err) } // update to the new current updated, err := store.SetCurrentUpdated(newSource) if utiltest.SkipRest(t, fmt.Sprintf("%q -> %q", c.current, c.newCurrent), err, c.err) { continue } // check that SetCurrentUpdated correctly reports whether the current checkpoint changed if c.expectUpdated != updated { t.Errorf("case %q -> %q, expect %v but got %v", c.current, c.newCurrent, c.expectUpdated, updated) } // check that curFile is saved by SetCurrentUpdated as we expect data := readTestSourceFile(t, store.fs, curFile) if c.current == c.newCurrent { // same UID should leave file unchanged if expectSource != string(data) { t.Errorf("case %q -> %q, expect current source file to contain %q, but got %q", c.current, c.newCurrent, expectSource, string(data)) } } else if expectNewSource != string(data) { // otherwise expect the file to change t.Errorf("case %q -> %q, expect current source file to contain %q, but got %q", c.current, c.newCurrent, expectNewSource, string(data)) } } } func TestFsStoreSetLastKnownGood(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } const uid = "uid" expect := fmt.Sprintf(`{"kind":"NodeConfigSource","apiVersion":"v1","configMapRef":{"namespace":"namespace","name":"name","uid":"%s"}}%s`, uid, "\n") source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{ Name: "name", Namespace: "namespace", UID: types.UID(uid)}}) if err != nil { t.Fatalf("unexpected error: %v", err) } // save the last known good source if err := store.SetLastKnownGood(source); err != nil { t.Fatalf("unexpected error: %v", err) } // check that the source saved as we would expect data := readTestSourceFile(t, store.fs, lkgFile) if expect != string(data) { t.Errorf("expect last-known-good source file to contain %q, but got %q", expect, string(data)) } } func TestFsStoreReset(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{Name: "name", Namespace: "namespace", UID: "uid"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } otherSource, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{Name: "other-name", Namespace: "namespace", UID: "other-uid"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } cases := []struct { desc string current checkpoint.RemoteConfigSource lastKnownGood checkpoint.RemoteConfigSource updated bool }{ {"nil -> nil", nil, nil, false}, {"source -> nil", source, nil, true}, {"nil -> source", nil, source, false}, {"source -> source", source, source, true}, {"source -> otherSource", source, otherSource, true}, {"otherSource -> source", otherSource, source, true}, } for _, c := range cases { // manually save the sources to their respective files saveTestSourceFile(t, store.fs, curFile, c.current) saveTestSourceFile(t, store.fs, lkgFile, c.lastKnownGood) // reset updated, err := store.Reset() if err != nil { t.Fatalf("unexpected error: %v", err) } // make sure the files were emptied if size := testSourceFileSize(t, store.fs, curFile); size > 0 { t.Errorf("case %q, expect source file %q to be empty but got %d bytes", c.desc, curFile, size) } if size := testSourceFileSize(t, store.fs, lkgFile); size > 0 { t.Errorf("case %q, expect source file %q to be empty but got %d bytes", c.desc, lkgFile, size) } // make sure Current() and LastKnownGood() both return nil current, err := store.Current() if err != nil { t.Fatalf("unexpected error: %v", err) } lastKnownGood, err := store.LastKnownGood() if err != nil { t.Fatalf("unexpected error: %v", err) } if current != nil || lastKnownGood != nil { t.Errorf("case %q, expect nil for current and last-known-good checkpoints, but still have %q and %q, respectively", c.desc, current, lastKnownGood) } if c.updated != updated { t.Errorf("case %q, expect reset to return %t, but got %t", c.desc, c.updated, updated) } } } func TestFsStoreSourceFromFile(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ ConfigMapRef: &apiv1.ObjectReference{Name: "name", Namespace: "namespace", UID: "uid"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } cases := []struct { desc string expect checkpoint.RemoteConfigSource err string }{ {"default source", nil, ""}, {"non-default source", source, ""}, } const name = "some-source-file" for _, c := range cases { saveTestSourceFile(t, store.fs, name, c.expect) source, err := store.sourceFromFile(name) if utiltest.SkipRest(t, c.desc, err, c.err) { continue } if !checkpoint.EqualRemoteConfigSources(c.expect, source) { t.Errorf("case %q, expect %q but got %q", spew.Sdump(c.expect), spew.Sdump(c.expect), spew.Sdump(source)) } } } func TestFsStoreSetSourceFile(t *testing.T) { store, err := newInitializedFakeFsStore() if err != nil { t.Fatalf("failed to construct a store, error: %v", err) } source, _, err := checkpoint.NewRemoteConfigSource(&apiv1.NodeConfigSource{ConfigMapRef: &apiv1.ObjectReference{Name: "name", Namespace: "namespace", UID: "uid"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } cases := []struct { source checkpoint.RemoteConfigSource }{ {nil}, {source}, } const name = "some-source-file" for _, c := range cases { // set the source file err := store.setSourceFile(name, c.source) if err != nil { t.Fatalf("unable to set source file, error: %v", err) } // read back the file data := readTestSourceFile(t, store.fs, name) str := string(data) if c.source != nil { // expect the contents to match the encoding of the source data, err := c.source.Encode() expect := string(data) if err != nil { t.Fatalf("couldn't encode source, error: %v", err) } if expect != str { t.Errorf("case %q, expect %q but got %q", spew.Sdump(c.source), expect, str) } } else { // expect empty file expect := "" if expect != str { t.Errorf("case %q, expect %q but got %q", spew.Sdump(c.source), expect, str) } } } } func readTestCheckpointFile(t *testing.T, fs utilfs.Filesystem, uid string) []byte { data, err := fs.ReadFile(filepath.Join(testCheckpointsDir, uid)) if err != nil { t.Fatalf("unable to read test checkpoint file, error: %v", err) } return data } func saveTestCheckpointFile(t *testing.T, fs utilfs.Filesystem, cpt checkpoint.Checkpoint) { data, err := cpt.Encode() if err != nil { t.Fatalf("unable to encode test checkpoint, error: %v", err) } fmt.Println(cpt.UID()) err = utilfiles.ReplaceFile(fs, filepath.Join(testCheckpointsDir, cpt.UID()), data) if err != nil { t.Fatalf("unable to save test checkpoint file, error: %v", err) } } func readTestSourceFile(t *testing.T, fs utilfs.Filesystem, relPath string) []byte { data, err := fs.ReadFile(filepath.Join(testCheckpointsDir, relPath)) if err != nil { t.Fatalf("unable to read test source file, error: %v", err) } return data } func saveTestSourceFile(t *testing.T, fs utilfs.Filesystem, relPath string, source checkpoint.RemoteConfigSource) { if source != nil { data, err := source.Encode() if err != nil { t.Fatalf("unable to save test source file, error: %v", err) } err = utilfiles.ReplaceFile(fs, filepath.Join(testCheckpointsDir, relPath), data) if err != nil { t.Fatalf("unable to save test source file, error: %v", err) } } else { err := utilfiles.ReplaceFile(fs, filepath.Join(testCheckpointsDir, relPath), []byte{}) if err != nil { t.Fatalf("unable to save test source file, error: %v", err) } } } func testSourceFileSize(t *testing.T, fs utilfs.Filesystem, relPath string) int64 { info, err := fs.Stat(filepath.Join(testCheckpointsDir, relPath)) if err != nil { t.Fatalf("unexpected error: %v", err) } return info.Size() }