Refactor Loader interface

This commit is contained in:
Sean Sullivan
2018-02-23 16:48:28 -08:00
parent c7ec56e3e5
commit 2803503e3a
4 changed files with 216 additions and 142 deletions

View File

@@ -17,32 +17,75 @@ limitations under the License.
package loader
import (
"fmt"
"os"
"path/filepath"
"k8s.io/kubectl/pkg/kinflate/util/fs"
)
// Implements internal interface schemeLoader.
const currentDir = "."
// Internal implementation of SchemeLoader interface.
type fileLoader struct {
fs fs.FileSystem
}
func newFileLoader(fs fs.FileSystem) (schemeLoader, error) {
return &fileLoader{fs: fs}, nil
// NewFileLoader returns a SchemeLoader to handle a file system.
func NewFileLoader(fs fs.FileSystem) SchemeLoader {
return &fileLoader{fs: fs}
}
// Join the root path with the location path.
func (l *fileLoader) fullLocation(root string, location string) string {
// Is the location calculated with the root and location params a full file path.
func (l *fileLoader) IsScheme(root string, location string) bool {
fullFilePath, err := l.FullLocation(root, location)
if err != nil {
return false
}
return filepath.IsAbs(fullFilePath)
}
// Returns the directory of the calculated full file path.
// Example: "/home/seans/project", "subdir/file.txt" -> "/home/seans/project/subdir".
func (l *fileLoader) Root(root string, location string) (string, error) {
fullFilePath, err := l.FullLocation(root, location)
if err != nil {
return "", err
}
return filepath.Dir(fullFilePath), nil
}
// If location is a full file path, then ignore root. If location is relative, then
// join the root path with the location path. Either root or location can be empty,
// but not both.
// Example: "/home/seans/project", "subdir/file.txt" -> "/home/seans/project/subdir/file.txt".
func (l *fileLoader) FullLocation(root string, location string) (string, error) {
// First, validate the parameters
if len(root) == 0 && len(location) == 0 {
return "", fmt.Errorf("Unable to calculate full location: root and location empty")
}
// Special case current directory, expanding to full file path.
if location == currentDir {
currentDir, err := os.Getwd()
if err != nil {
return "", err
}
location = currentDir
}
// Assume the location is an full file path. If not, then join root with location.
fullLocation := location
if !filepath.IsAbs(location) {
fullLocation = filepath.Join(root, location)
}
return fullLocation
return fullLocation, nil
}
// Load returns the bytes from reading a file at fullFilePath.
// Implements the Loader interface.
func (l *fileLoader) load(fullFilePath string) ([]byte, error) {
// TODO: Check that fullFilePath is an absolute file path.
func (l *fileLoader) Load(fullFilePath string) ([]byte, error) {
// Validate path to load from is a full file path.
if !filepath.IsAbs(fullFilePath) {
return nil, fmt.Errorf("Attempting to load file without full file path: %s\n", fullFilePath)
}
return l.fs.ReadFile(fullFilePath)
}

View File

@@ -1,54 +0,0 @@
/*
Copyright 2018 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 loader
import (
"reflect"
"testing"
"k8s.io/kubectl/pkg/kinflate/util/fs"
)
func TestFileLoaderHappyPath(t *testing.T) {
fakefs := fs.MakeFakeFS()
location := "foo"
content := []byte("bar")
fakefs.WriteFile(location, content)
l, err := newFileLoader(fakefs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
b, err := l.load(location)
if err != nil {
t.Fatalf("unexpected error in Load: %v", err)
}
if !reflect.DeepEqual(b, content) {
t.Fatalf("expected %s, but got %s", content, b)
}
}
func TestFileLoaderFileNotFound(t *testing.T) {
fakefs := fs.MakeFakeFS()
l, err := newFileLoader(fakefs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = l.load("/path/does/not/exist")
if err == nil {
t.Fatal("expected error in Load, but no error returned")
}
}

139
loader.go
View File

@@ -16,87 +16,82 @@ limitations under the License.
package loader
import (
"fmt"
import "fmt"
"k8s.io/kubectl/pkg/kinflate/util/fs"
)
// Loader abstracts how bytes are read for manifest, resource, patch, or other
// files. Each Loader is tightly coupled with the location of a manifest file.
// So each file without an absolute path is located relative to the
// manifest file it was read from. Each Load() call is relative to this manifest
// location (referenced as root). The Loader hides how to read bytes from different
// "schemes" (e.g. file, url, or git).
// Loader interface exposes methods to read bytes in a scheme-agnostic manner.
// The Loader encapsulating a root location to calculate where to read from.
type Loader interface {
// Clones new Loader for a new app/package with its own manifest from the current
// Loader. The "newRoot" can be relative or absolute. If it's relative, the new
// Loader root is calculated from the current Loader root. Can be a file or directory.
// If it's a file, then the base directory is used for root calculation.
// Root returns the scheme-specific string representing the root location for this Loader.
Root() string
// New returns Loader located at newRoot.
New(newRoot string) (Loader, error)
// Returns the bytes at location or an error. If it's a relative path, then
// the location is expanded using the Loader root.
// Example: returns YAML bytes at location "/home/seans/project/service.yaml".
// Load returns the bytes read from the location or an error.
Load(location string) ([]byte, error)
}
// Private implmentation of Loader interface.
type loaderImpl struct {
root string
fs fs.FileSystem
// http client for URL loading
// git client for Git loading
root string
schemes []SchemeLoader
}
// RootLoader initializes the first Loader, with the initial root location.
func RootLoader(root string, fs fs.FileSystem) Loader {
// TODO: Validate the root
return &loaderImpl{root: root, fs: fs}
}
// New clones a new Loader with a new absolute root path.
func (l *loaderImpl) New(newRoot string) (Loader, error) {
loader, err := l.getSchemeLoader(newRoot)
if err != nil {
return nil, err
}
return &loaderImpl{root: loader.fullLocation(l.root, newRoot), fs: l.fs}, nil
}
// Load returns the bytes at the specified location.
// Implemented by getting a scheme-specific structure to
// load the bytes.
func (l *loaderImpl) Load(location string) ([]byte, error) {
loader, err := l.getSchemeLoader(location)
if err != nil {
return nil, err
}
fullLocation := loader.fullLocation(l.root, location)
return loader.load(fullLocation)
}
// Helper function to parse scheme from location parameter and return
func (l *loaderImpl) getSchemeLoader(location string) (schemeLoader, error) {
// FIXME: First check the scheme of root location.
switch {
case isFilePath(location):
return newFileLoader(l.fs)
default:
return nil, fmt.Errorf("unknown scheme: %v", location)
}
}
// Parses the location to determine if it is a file path.
func isFilePath(location string) bool {
return true
}
/////////////////////////////////////////////////
// Internal interface for specific type of loader
// Examples: fileLoader, HttpLoader, or GitLoader
type schemeLoader interface {
// Interface for different types of loaders (e.g. fileLoader, httpLoader, etc.)
type SchemeLoader interface {
// Does this location correspond to this scheme.
IsScheme(root string, location string) bool
// Calulcate a new root.
Root(root string, location string) (string, error)
// Combines the root and path into a full location string.
fullLocation(root string, path string) string
// Must be a full, non-relative location string.
load(location string) ([]byte, error)
FullLocation(root string, path string) (string, error)
// Load bytes at scheme-specific location or an error.
Load(location string) ([]byte, error)
}
const emptyRoot = ""
// Init initializes the first loader with the supported schemes.
// Example schemes: fileLoader, httpLoader, gitLoader.
func Init(schemes []SchemeLoader) Loader {
return &loaderImpl{root: emptyRoot, schemes: schemes}
}
// Root returns the scheme-specific root location for this Loader.
func (l *loaderImpl) Root() string {
return l.root
}
// Returns a new Loader rooted at newRoot.
func (l *loaderImpl) New(newRoot string) (Loader, error) {
scheme, err := l.getSchemeLoader(newRoot)
if err != nil {
return nil, err
}
root, err := scheme.Root(l.root, newRoot)
if err != nil {
return nil, err
}
return &loaderImpl{root: root, schemes: l.schemes}, nil
}
// Load returns all the bytes read from scheme-specific location or an error.
func (l *loaderImpl) Load(location string) ([]byte, error) {
scheme, err := l.getSchemeLoader(location)
if err != nil {
return nil, err
}
fullLocation, err := scheme.FullLocation(l.root, location)
if err != nil {
return nil, err
}
return scheme.Load(fullLocation)
}
// Helper function to parse scheme from location parameter.
func (l *loaderImpl) getSchemeLoader(location string) (SchemeLoader, error) {
for _, scheme := range l.schemes {
if scheme.IsScheme(l.root, location) {
return scheme, nil
}
}
return nil, fmt.Errorf("Unknown Scheme: %s, %s\n", l.root, location)
}

View File

@@ -17,23 +17,113 @@ limitations under the License.
package loader
import (
"path/filepath"
"reflect"
"testing"
"k8s.io/kubectl/pkg/kinflate/util/fs"
)
var rootDir = "/home/seans/project"
var rootFile = "file.yaml"
var rootFilePath = filepath.Join(rootDir, rootFile)
var rootFileContent = []byte("This is a yaml file")
var subDirectory = "subdir"
var subDirectoryPath = filepath.Join(subDirectory, rootFile)
var subDirectoryContent = []byte("Subdirectory file content")
var anotherRootDir = "/home/seans/project2"
var anotherFilePath = filepath.Join(anotherRootDir, rootFile)
var anotherFileContent = []byte("This is another yaml file")
func TestLoader_Root(t *testing.T) {
rootLoader := initializeRootLoader()
_, err := rootLoader.New("")
if err == nil {
t.Fatalf("Expected error for empty root location not returned")
}
_, err = rootLoader.New("https://google.com/project")
if err == nil {
t.Fatalf("Expected error for unknown scheme not returned")
}
loader, err := rootLoader.New(rootFilePath)
if err != nil {
t.Fatalf("Unexpected in New(): %v\n", err)
}
if rootDir != loader.Root() {
t.Fatalf("Incorrect Loader Root: %s\n", loader.Root())
}
subLoader, err := loader.New(subDirectoryPath)
if err != nil {
t.Fatalf("Unexpected in New(): %v\n", err)
}
if filepath.Join(rootDir, subDirectory) != subLoader.Root() {
t.Fatalf("Incorrect Loader Root: %s\n", subLoader.Root())
}
anotherLoader, err := loader.New(anotherFilePath)
if err != nil {
t.Fatalf("Unexpected in New(): %v\n", err)
}
if anotherRootDir != anotherLoader.Root() {
t.Fatalf("Incorrect Loader Root: %s\n", anotherLoader.Root())
}
currentDirLoader, err := loader.New(".")
if err != nil {
t.Fatalf("Unexpected in New(): %v\n", err)
}
if !filepath.IsAbs(currentDirLoader.Root()) {
t.Fatalf("Incorrect Loader Root: %s\n", currentDirLoader.Root())
}
}
func TestLoader_Load(t *testing.T) {
fakefs := fs.MakeFakeFS()
location := "/home/seans/project/Kube-manifest.yaml"
content := []byte("This is a kinflate manifest")
fakefs.WriteFile(location, content)
loader := RootLoader(location, fakefs)
manifestBytes, err := loader.Load(location)
rootLoader := initializeRootLoader()
loader, err := rootLoader.New(rootFilePath)
if err != nil {
t.Fatalf("Unexpected in New(): %v\n", err)
}
fileBytes, err := loader.Load(rootFilePath)
if err != nil {
t.Fatalf("Unexpected error in Load(): %v", err)
}
if !reflect.DeepEqual(content, manifestBytes) {
t.Fatalf("expected %s, but got %s", content, manifestBytes)
if !reflect.DeepEqual(rootFileContent, fileBytes) {
t.Fatalf("Load failed. Expected %s, but got %s", rootFileContent, fileBytes)
}
fileBytes, err = loader.Load(subDirectoryPath)
if err != nil {
t.Fatalf("Unexpected error in Load(): %v", err)
}
if !reflect.DeepEqual(subDirectoryContent, fileBytes) {
t.Fatalf("Load failed. Expected %s, but got %s", subDirectoryContent, fileBytes)
}
fileBytes, err = loader.Load(anotherFilePath)
if err != nil {
t.Fatalf("Unexpected error in Load(): %v", err)
}
if !reflect.DeepEqual(anotherFileContent, fileBytes) {
t.Fatalf("Load failed. Expected %s, but got %s", anotherFileContent, fileBytes)
}
}
func initializeRootLoader() Loader {
fs := initializeFakeFilesystem()
var schemes []SchemeLoader
schemes = append(schemes, NewFileLoader(fs))
rootLoader := Init(schemes)
return rootLoader
}
func initializeFakeFilesystem() fs.FileSystem {
fakefs := fs.MakeFakeFS()
fakefs.WriteFile(rootFilePath, rootFileContent)
fakefs.WriteFile(filepath.Join(rootDir, subDirectoryPath), subDirectoryContent)
fakefs.WriteFile(anotherFilePath, anotherFileContent)
return fakefs
}