package manifest import ( "bytes" _ "embed" "errors" "io/fs" "os" "strings" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) var ( //go:embed testdata/test-pod.yaml podBytes []byte //go:embed testdata/embed/updated-test-pod.yaml updatedPodBytes []byte ) var podObject = &corev1.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Pod", }, ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "test-container", Image: "test-image:1.14.2", Ports: []corev1.ContainerPort{ { ContainerPort: 80, }, }, }, }, }, } func updatedPodObject() *corev1.Pod { pod := podObject.DeepCopy() pod.Name = "updated-test-pod" return pod } var ( testPodReadpath = "/test-pod.yaml" testPodWritepath = "/test-pod-write.yaml" invalidPodFilepath = "/invalid.yaml" ) func TestMain(m *testing.M) { os.Exit(m.Run()) } func TestRead(t *testing.T) { memFs, err := createMemFs(afero.NewOsFs()) require.NoError(t, err) testCases := map[string]struct { manifest Manifest syncMode bool expectedBytes []byte expectedMode fs.FileMode expectError bool }{ "WithModeSync_Success": { manifest: New(memFs, testPodReadpath, &corev1.Pod{}, 0666), syncMode: true, expectedBytes: podBytes, expectedMode: fs.FileMode(0660), expectError: false, }, "WithoutModeSync_Success": { manifest: New(memFs, testPodReadpath, &corev1.Pod{}, 0666), syncMode: false, expectedBytes: podBytes, expectedMode: fs.FileMode(0666), expectError: false, }, "FileNotFound_Failure": { manifest: New(memFs, invalidPodFilepath, &corev1.Pod{}, 0666), expectError: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { err := tc.manifest.Read(tc.syncMode) if tc.expectError { require.Error(t, err) return } require.NoError(t, err) gotBytes, err := tc.manifest.Bytes() require.NoError(t, err) assert.Equal(t, tc.expectedBytes, gotBytes) assert.Equal(t, tc.expectedMode, tc.manifest.mode) }) } } func TestLoad(t *testing.T) { testCases := map[string]struct { manifest Manifest inputBytes []byte expectedObj *corev1.Pod expectError bool }{ "Success": { manifest: New(nil, "", &corev1.Pod{}, 0), inputBytes: podBytes, expectedObj: podObject, expectError: false, }, "InvalidYAML_Failure": { manifest: New(nil, "", &corev1.Pod{}, 0), inputBytes: []byte("hello"), expectError: true, }, "NoManifests_Failure": { manifest: New(nil, "", &corev1.Pod{}, 0), inputBytes: []byte{}, expectError: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { err := tc.manifest.Load(tc.inputBytes) if tc.expectError { require.Error(t, err) return } require.NoError(t, err) gotObj := tc.manifest.Content().(*corev1.Pod) assert.Equal(t, tc.expectedObj, gotObj) }) } } func TestWrite(t *testing.T) { memFs, err := createMemFs(afero.NewOsFs()) require.NoError(t, err) testCases := map[string]struct { manifest Manifest inputBytes []byte expectedBytes []byte expectedMode fs.FileMode expectError bool }{ "Success": { manifest: New(memFs, testPodWritepath, &corev1.Pod{}, 0666), inputBytes: podBytes, expectedBytes: podBytes, expectedMode: fs.FileMode(0666), expectError: false, }, "NoBaseObject_Failure": { manifest: New(memFs, testPodWritepath, nil, 0666), inputBytes: []byte{}, expectError: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { if !bytes.Equal(tc.inputBytes, []byte{}) { err := tc.manifest.Load(tc.inputBytes) require.NoError(t, err) } err := tc.manifest.Write() if tc.expectError { require.Error(t, err) return } require.NoError(t, err) gotBytes, err := afero.ReadFile(memFs, testPodWritepath) require.NoError(t, err) assert.Equal(t, tc.expectedBytes, gotBytes) assert.Equal(t, tc.expectedMode, tc.manifest.mode) require.NoError(t, memFs.RemoveAll(testPodWritepath)) }) } } func TestWithCreate(t *testing.T) { memFs, err := createMemFs(afero.NewOsFs()) require.NoError(t, err) testCases := map[string]struct { manifest Manifest fn func(runtime.Object) error expectedBytes []byte expectedMode fs.FileMode expectError bool }{ "DefaultMode_Success": { manifest: New(memFs, testPodWritepath, &corev1.Pod{}, 0), fn: func(obj runtime.Object) error { podObject.DeepCopyInto(obj.(*corev1.Pod)) return nil }, expectedBytes: podBytes, expectedMode: fs.FileMode(0600), expectError: false, }, "OverwriteMode_Success": { manifest: New(memFs, testPodWritepath, &corev1.Pod{}, 0666), fn: func(obj runtime.Object) error { podObject.DeepCopyInto(obj.(*corev1.Pod)) return nil }, expectedBytes: podBytes, expectedMode: fs.FileMode(0666), expectError: false, }, "Failure": { manifest: New(memFs, testPodWritepath, nil, 0666), fn: func(_ runtime.Object) error { return errors.New("force error") }, expectError: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { err := tc.manifest.WithCreate(tc.fn) switch tc.expectError { case true: require.Error(t, err) case false: require.NoError(t, err) } gotBytes, err := afero.ReadFile(memFs, testPodWritepath) switch tc.expectError { case true: require.Error(t, err) return case false: require.NoError(t, err) } assert.Equal(t, tc.expectedBytes, gotBytes) assert.Equal(t, tc.expectedMode, tc.manifest.mode) require.NoError(t, memFs.RemoveAll(testPodWritepath)) }) } } func TestWithUpdate(t *testing.T) { memFs, err := createMemFs(afero.NewOsFs()) require.NoError(t, err) testCases := map[string]struct { manifest Manifest fn func(runtime.Object) error expectedBytes []byte expectedMode fs.FileMode expectError bool }{ "PersistMode_Success": { manifest: New(memFs, testPodWritepath, &corev1.Pod{}, 0), fn: func(obj runtime.Object) error { updatedPodObject().DeepCopyInto(obj.(*corev1.Pod)) return nil }, expectedBytes: updatedPodBytes, expectedMode: fs.FileMode(0660), expectError: false, }, "OverwriteMode_Success": { manifest: New(memFs, testPodWritepath, &corev1.Pod{}, 0666), fn: func(obj runtime.Object) error { updatedPodObject().DeepCopyInto(obj.(*corev1.Pod)) return nil }, expectedBytes: updatedPodBytes, expectedMode: fs.FileMode(0666), expectError: false, }, "Failure": { manifest: New(memFs, testPodWritepath, &corev1.Pod{}, 0666), fn: func(_ runtime.Object) error { return errors.New("force error") }, expectedBytes: podBytes, expectError: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { require.NoError(t, afero.WriteFile(memFs, testPodWritepath, podBytes, 0660)) err := tc.manifest.WithUpdate(tc.fn) switch tc.expectError { case true: require.Error(t, err) case false: require.NoError(t, err) } gotBytes, err := afero.ReadFile(memFs, testPodWritepath) switch tc.expectError { case true: assert.Equal(t, tc.expectedBytes, gotBytes) return case false: require.NoError(t, err) } assert.Equal(t, tc.expectedBytes, gotBytes) assert.Equal(t, tc.expectedMode, tc.manifest.mode) require.NoError(t, memFs.RemoveAll(testPodWritepath)) }) } } func createMemFs(osFs afero.Fs) (afero.Fs, error) { memFs := afero.NewMemMapFs() return memFs, afero.Walk(osFs, "testdata", func(path string, info fs.FileInfo, err error) error { if strings.Contains(path, "/embed/") { return nil } if err != nil { return err } fsPath := strings.ReplaceAll(path, "testdata", "") if info.IsDir() { return memFs.MkdirAll(fsPath, info.Mode()) } content, err := afero.ReadFile(osFs, path) if err != nil { return err } return afero.WriteFile(memFs, fsPath, content, 0660) }) }