github.com/replicatedhq/ship@v0.55.0/pkg/filetree/loader.go (about)

     1  package filetree
     2  
     3  import (
     4  	"os"
     5  	"path"
     6  	"strings"
     7  
     8  	"github.com/go-kit/kit/log"
     9  	"github.com/go-kit/kit/log/level"
    10  	"github.com/pkg/errors"
    11  	"github.com/replicatedhq/ship/pkg/state"
    12  	"github.com/spf13/afero"
    13  	"sigs.k8s.io/kustomize/k8sdeps/kunstruct"
    14  	"sigs.k8s.io/kustomize/pkg/resource"
    15  )
    16  
    17  const (
    18  	CustomResourceDefinition = "CustomResourceDefinition"
    19  	PatchesFolder            = "overlays"
    20  	ResourcesFolder          = "resources"
    21  )
    22  
    23  // A Loader returns a struct representation
    24  // of a filesystem directory tree
    25  type Loader interface {
    26  	LoadTree(root string) (*Node, error)
    27  	// someday this should return an overlay too
    28  	LoadFile(root string, path string) ([]byte, error)
    29  }
    30  
    31  // NewLoader builds an aferoLoader, used with dig
    32  func NewLoader(
    33  	fs afero.Afero,
    34  	logger log.Logger,
    35  	stateManager state.Manager,
    36  ) Loader {
    37  	return &aferoLoader{
    38  		FS:           fs,
    39  		Logger:       logger,
    40  		StateManager: stateManager,
    41  	}
    42  }
    43  
    44  type aferoLoader struct {
    45  	Logger        log.Logger
    46  	FS            afero.Afero
    47  	StateManager  state.Manager
    48  	excludedBases map[string]string
    49  	patches       map[string]string
    50  	resources     map[string]string
    51  }
    52  
    53  func (a *aferoLoader) loadShipOverlay() error {
    54  	currentState, err := a.StateManager.CachedState()
    55  	if err != nil {
    56  		return errors.Wrap(err, "failed to load state")
    57  	}
    58  
    59  	kustomize := currentState.CurrentKustomize()
    60  	if kustomize == nil {
    61  		kustomize = &state.Kustomize{}
    62  	}
    63  
    64  	shipOverlay := kustomize.Ship()
    65  	baseMap := make(map[string]string)
    66  	for _, base := range shipOverlay.ExcludedBases {
    67  		baseMap[base] = base
    68  	}
    69  	a.excludedBases = baseMap
    70  	a.patches = shipOverlay.Patches
    71  	a.resources = shipOverlay.Resources
    72  	return nil
    73  }
    74  
    75  func (a *aferoLoader) LoadTree(root string) (*Node, error) {
    76  	if err := a.loadShipOverlay(); err != nil {
    77  		return nil, errors.Wrapf(err, "load overlays")
    78  	}
    79  
    80  	fs := afero.Afero{Fs: afero.NewBasePathFs(a.FS, root)}
    81  
    82  	files, err := fs.ReadDir("/")
    83  	if err != nil {
    84  		return nil, errors.Wrapf(err, "read dir %q", root)
    85  	}
    86  
    87  	rootNode := Node{
    88  		Path:     "/",
    89  		Name:     "/",
    90  		Children: []Node{},
    91  	}
    92  	patchesRootNode := Node{
    93  		Path:     "/",
    94  		Name:     PatchesFolder,
    95  		Children: []Node{},
    96  	}
    97  	resourceRootNode := Node{
    98  		Path:     "/",
    99  		Name:     ResourcesFolder,
   100  		Children: []Node{},
   101  	}
   102  
   103  	populatedBase, err := a.loadTree(fs, rootNode, files)
   104  	if err != nil {
   105  		return nil, errors.Wrap(err, "load tree")
   106  	}
   107  
   108  	populatedPatches := a.loadOverlayTree(patchesRootNode, a.patches)
   109  	populatedResources := a.loadOverlayTree(resourceRootNode, a.resources)
   110  
   111  	children := []Node{populatedBase}
   112  
   113  	if len(populatedPatches.Children) != 0 {
   114  		children = append(children, populatedPatches)
   115  	}
   116  
   117  	if len(populatedResources.Children) != 0 {
   118  		children = append(children, populatedResources)
   119  	}
   120  
   121  	return &Node{
   122  		Path:     "/",
   123  		Name:     "/",
   124  		Children: children,
   125  	}, nil
   126  }
   127  
   128  // todo move this to a new struct or something
   129  func (a *aferoLoader) LoadFile(root string, file string) ([]byte, error) {
   130  	fs := afero.Afero{Fs: afero.NewBasePathFs(a.FS, root)}
   131  	contents, err := fs.ReadFile(file)
   132  	if err != nil {
   133  		return []byte{}, errors.Wrap(err, "read file")
   134  	}
   135  
   136  	return contents, nil
   137  }
   138  
   139  func (a *aferoLoader) loadTree(fs afero.Afero, current Node, files []os.FileInfo) (Node, error) {
   140  	if len(files) == 0 {
   141  		return current, nil
   142  	}
   143  
   144  	file, rest := files[0], files[1:]
   145  	filePath := path.Join(current.Path, file.Name())
   146  
   147  	// no thanks
   148  	if isSymlink(file) {
   149  		level.Debug(a.Logger).Log("event", "symlink.skip", "file", filePath)
   150  		return a.loadTree(fs, current, rest)
   151  	}
   152  
   153  	if !file.IsDir() {
   154  		_, hasOverlay := a.patches[filePath]
   155  
   156  		fileB, err := fs.ReadFile(filePath)
   157  		if err != nil {
   158  			return current, errors.Wrapf(err, "read file %s", file.Name())
   159  		}
   160  
   161  		_, exists := a.excludedBases[filePath]
   162  		return a.loadTree(fs, current.withChild(Node{
   163  			Name:        file.Name(),
   164  			Path:        filePath,
   165  			HasOverlay:  hasOverlay,
   166  			IsSupported: IsSupported(fileB),
   167  			IsExcluded:  exists,
   168  		}), rest)
   169  	}
   170  
   171  	subFiles, err := fs.ReadDir(filePath)
   172  	if err != nil {
   173  		return current, errors.Wrapf(err, "read dir %q", file.Name())
   174  	}
   175  
   176  	subTree := Node{
   177  		Name:     file.Name(),
   178  		Path:     filePath,
   179  		Children: []Node{},
   180  	}
   181  
   182  	subTreeLoaded, err := a.loadTree(fs, subTree, subFiles)
   183  	if err != nil {
   184  		return current, errors.Wrapf(err, "load tree %q", file.Name())
   185  	}
   186  
   187  	return a.loadTree(fs, current.withChild(subTreeLoaded), rest)
   188  }
   189  
   190  func isSymlink(file os.FileInfo) bool {
   191  	return file.Mode()&os.ModeSymlink != 0
   192  }
   193  
   194  func IsSupported(file []byte) bool {
   195  	resourceFactory := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
   196  
   197  	resources, err := resourceFactory.SliceFromBytes(file)
   198  	if err != nil {
   199  		return false
   200  	}
   201  	if len(resources) != 1 {
   202  		return false
   203  	}
   204  	r := resources[0]
   205  
   206  	// any kind but CRDs are supported
   207  	return r.GetKind() != CustomResourceDefinition
   208  }
   209  
   210  func (n Node) withChild(child Node) Node {
   211  	return Node{
   212  		Name:        n.Name,
   213  		Path:        n.Path,
   214  		Children:    append(n.Children, child),
   215  		IsSupported: n.IsSupported,
   216  		HasOverlay:  n.HasOverlay,
   217  	}
   218  }
   219  
   220  func (a *aferoLoader) loadOverlayTree(kustomizationNode Node, files map[string]string) Node {
   221  	filledTree := &kustomizationNode
   222  	for patchPath := range files {
   223  		splitPatchPath := strings.Split(patchPath, "/")[1:]
   224  		filledTree = a.createOverlayNode(filledTree, splitPatchPath)
   225  	}
   226  	return *filledTree
   227  }
   228  
   229  func (a *aferoLoader) createOverlayNode(kustomizationNode *Node, pathToOverlay []string) *Node {
   230  	if len(pathToOverlay) == 0 {
   231  		return kustomizationNode
   232  	}
   233  
   234  	pathToMatch, restOfPath := pathToOverlay[0], pathToOverlay[1:]
   235  	filePath := path.Join(kustomizationNode.Path, pathToMatch)
   236  
   237  	for i := range kustomizationNode.Children {
   238  		if kustomizationNode.Children[i].Path == pathToMatch || kustomizationNode.Children[i].Name == pathToMatch {
   239  			a.createOverlayNode(&kustomizationNode.Children[i], restOfPath)
   240  			return kustomizationNode
   241  		}
   242  	}
   243  
   244  	nextNode := Node{
   245  		Name: pathToMatch,
   246  		Path: filePath,
   247  	}
   248  	loadedChild := a.createOverlayNode(&nextNode, restOfPath)
   249  	kustomizationNode.Children = append(kustomizationNode.Children, *loadedChild)
   250  	return kustomizationNode
   251  }