Skip to content

Commit

Permalink
Support all kustomization files (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpreese authored May 28, 2019
1 parent a9cf1e8 commit 43c9b29
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 57 deletions.
10 changes: 4 additions & 6 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ package main

import (
"fmt"
"os"

"github.com/jpreese/kustomize-graph/pkg/graph"
"github.com/jpreese/kustomize-graph/pkg/kustomizationgraph"
)

func main() {
dependencyGraph, err := graph.GenerateKustomizeGraph()
graph, err := kustomizationgraph.New("main").Generate()
if err != nil {
fmt.Println(err)
os.Exit(1)
panic(err)
}

fmt.Print(dependencyGraph)
fmt.Print(graph)
}
68 changes: 46 additions & 22 deletions pkg/kustomizationfile/kustomizationfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,64 @@ type KustomizationFile struct {
Resources []string `yaml:"resources"`
Patches []string `yaml:"patches"`
PatchesStrategicMerge []string `yaml:"patchesStrategicMerge"`
}

MissingResources []string
// KustomizationFileNames represents a list of allowed filenames that
// kustomize searches for
var KustomizationFileNames = []string {
"kustomization.yaml",
"kustomization.yml",
"Kustomization",
}

type kustomizationFileContext struct {
fileSystem afero.Fs
}

// DefaultContext returns the context to interact with kustomization files
func DefaultContext() *kustomizationFileContext {
// New returns a new context to interact with kustomization files
func New() *kustomizationFileContext {
defaultFileSystem := afero.NewOsFs()

return &kustomizationFileContext{
fileSystem: defaultFileSystem,
}
return NewFromFileSystem(defaultFileSystem)
}

// ContextFromFileSystem returns a context based on the given filesystem
func ContextFromFileSystem(fileSystem afero.Fs) *kustomizationFileContext {
// NewFromFileSystem creates a context to interact with kustomization files from a provided file system
func NewFromFileSystem(fileSystem afero.Fs) *kustomizationFileContext {
return &kustomizationFileContext{
fileSystem: fileSystem,
}
}

// Get attempts to read a kustomization.yaml file
func (k *kustomizationFileContext) Get(filePath string) (*KustomizationFile, error) {
// GetFromDirectory attempts to read a kustomization.yaml file from the given directory
func (k *kustomizationFileContext) GetFromDirectory(directoryPath string) (*KustomizationFile, error) {
var kustomizationFile KustomizationFile
kustomizationFilePath := filepath.ToSlash(path.Join(filePath, "kustomization.yaml"))

fileUtility := &afero.Afero{Fs: k.fileSystem}

fileFoundCount := 0
kustomizationFilePath := ""
for _, kustomizationFile := range KustomizationFileNames {
currentPath := path.Join(directoryPath, kustomizationFile)

exists, err := fileUtility.Exists(currentPath)
if err != nil {
return nil, errors.Wrapf(err, "Could not check if file %v exists", currentPath)
}

if exists {
kustomizationFilePath = currentPath
fileFoundCount++
}
}

if kustomizationFilePath == "" {
return nil, errors.Wrapf(errors.New("Missing kustomization file"), "Directory %v did not contain a valid kustomization file", directoryPath)
}

if fileFoundCount > 1 {
return nil, errors.Wrapf(errors.New("Too many kustomization files"), "Directory %v contained more than one kustomization file", directoryPath)
}

kustomizationFileBytes, err := fileUtility.ReadFile(kustomizationFilePath)
if err != nil {
return nil, errors.Wrapf(err, "Could not read file %s", kustomizationFilePath)
Expand All @@ -55,26 +83,21 @@ func (k *kustomizationFileContext) Get(filePath string) (*KustomizationFile, err
return nil, errors.Wrapf(err, "Could not unmarshal yaml file %s", kustomizationFilePath)
}

missingResources, err := k.getMissingResources(filePath, &kustomizationFile)
if err != nil {
return nil, errors.Wrapf(err, "Could not get missing resources in path %s", kustomizationFilePath)
}

kustomizationFile.MissingResources = missingResources

return &kustomizationFile, nil
}

func (k *kustomizationFileContext) getMissingResources(filePath string, kustomizationFile *KustomizationFile) ([]string, error) {
// GetMissingResources returns a collection of resources that exist in the directory
// but are not defined in the given kustomization file
func (k *kustomizationFileContext) GetMissingResources(directoryPath string, kustomizationFile *KustomizationFile) ([]string, error) {
definedResources := []string{}
definedResources = append(definedResources, kustomizationFile.Resources...)
definedResources = append(definedResources, kustomizationFile.Patches...)
definedResources = append(definedResources, kustomizationFile.PatchesStrategicMerge...)

fileUtility := &afero.Afero{Fs: k.fileSystem}
directoryInfo, err := fileUtility.ReadDir(filePath)
directoryInfo, err := fileUtility.ReadDir(directoryPath)
if err != nil {
return nil, errors.Wrapf(err, "Could not read directory %s", filePath)
return nil, errors.Wrapf(err, "Could not read directory %s", directoryPath)
}

missingResources := []string{}
Expand All @@ -88,7 +111,8 @@ func (k *kustomizationFileContext) getMissingResources(filePath string, kustomiz
continue
}

if info.Name() == "kustomization.yaml" {
// Ignore the kustomization files
if existsInSlice(KustomizationFileNames, info.Name()) {
continue
}

Expand Down
11 changes: 8 additions & 3 deletions pkg/kustomizationfile/kustomizationfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ resources:
- a.yaml
`
afero.WriteFile(fakeFileSystem, "app/kustomization.yaml", []byte(fileContents), 0644)
kustomizationFile, _ := ContextFromFileSystem(fakeFileSystem).Get("app")
kustomizationFile, _ := NewFromFileSystem(fakeFileSystem).GetFromDirectory("app")

expected := "a.yaml"
actual := kustomizationFile.Resources[0]
Expand All @@ -49,12 +49,17 @@ func TestGetMissingResources(t *testing.T) {
afero.WriteFile(fakeFileSystem, "app/kustomization.yaml", []byte(""), 0644)
afero.WriteFile(fakeFileSystem, "app/excluded.yaml", []byte(""), 0644)

kustomizationFile, err := ContextFromFileSystem(fakeFileSystem).Get("app")
kustomizationFileContext := NewFromFileSystem(fakeFileSystem)
kustomizationFile, err := kustomizationFileContext.GetFromDirectory("app")
if err != nil {
t.Fatalf("An error occured while getting kustomization file %v", err)
}

actual, err := kustomizationFileContext.GetMissingResources("app", kustomizationFile)
if err != nil {
t.Fatalf("An error occured while getting missing resources %v", err)
}

actual := kustomizationFile.MissingResources
expected := []string{"excluded.yaml"}

if reflect.DeepEqual(actual, expected) == false {
Expand Down
49 changes: 31 additions & 18 deletions pkg/graph/graph.go → pkg/kustomizationgraph/kustomizationgraph.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package graph
package kustomizationgraph

import (
"github.com/awalterschulze/gographviz"
Expand All @@ -8,57 +8,71 @@ import (
"strings"
"os"

"github.com/spf13/afero"
"github.com/jpreese/kustomize-graph/pkg/kustomizationfile"
)

type kustomizationGraph struct {
*gographviz.Graph
fileSystem afero.Fs
}

// KustomizationFileGetter loads an environment to get kustomization files from
// KustomizationFileGetter gets kustomization files and kustomization file metadata
type KustomizationFileGetter interface {
Get(filePath string) (*kustomizationfile.KustomizationFile, error)
GetFromDirectory(directoryPath string) (*kustomizationfile.KustomizationFile, error)
GetMissingResources(directoryPath string, kustomizationFile *kustomizationfile.KustomizationFile) ([]string, error)
}

// NewGraph creates an unpopulated graph with the given name
func NewGraph(graphName string) *kustomizationGraph {
// New creates an unpopulated graph with the given name
func New(graphName string) *kustomizationGraph {
defaultFileSystem := afero.NewOsFs()
return NewFromFileSystem(defaultFileSystem, graphName)
}

// NewFromFileSystem creates an unpopulated graph with the given name using the given filesystem
func NewFromFileSystem(fileSystem afero.Fs, graphName string) *kustomizationGraph {
defaultGraph := gographviz.NewGraph()
defaultGraph.SetName(graphName)
defaultGraph.Directed = true

graph := &kustomizationGraph {
Graph: defaultGraph,
fileSystem: fileSystem,
}

return graph
}

// GenerateKustomizeGraph returns a DOT graph based on the dependencies
// Generate returns a DOT graph based on the dependencies
// from the kustomization.yaml file located in the current working directory
func GenerateKustomizeGraph() (string, error) {
func (g *kustomizationGraph) Generate() (string, error) {
workingDirectory, err := os.Getwd()
if err != nil {
return "", errors.Wrapf(err, "Unable to get current working directory")
}

graph := NewGraph("main")
kustomizationFileContext := kustomizationfile.DefaultContext()

err = graph.buildGraph(kustomizationFileContext, workingDirectory, "")
kustomizationFileContext := kustomizationfile.NewFromFileSystem(g.fileSystem)

err = g.buildGraph(kustomizationFileContext, workingDirectory, "")
if err != nil {
return "", errors.Wrapf(err, "Could not produce graph from directory %s", workingDirectory)
}

return graph.String(), nil
return g.String(), nil
}

func (g *kustomizationGraph) buildGraph(k KustomizationFileGetter, currentPath string, previousNode string) error {
kustomizationFile, err := k.Get(currentPath)
kustomizationFile, err := k.GetFromDirectory(currentPath)
if err != nil {
return errors.Wrapf(err, "Could not get kustomization file")
}

node, err := g.addNodeToGraph(currentPath, kustomizationFile)
missingResources, err := k.GetMissingResources(currentPath, kustomizationFile)
if err != nil {
return errors.Wrapf(err, "Could not get kustomization file missing resources")
}

node, err := g.addNodeToGraph(currentPath, missingResources)
if err != nil {
return errors.Wrapf(err, "Could not create node from path %s", currentPath)
}
Expand All @@ -85,14 +99,13 @@ func (g *kustomizationGraph) buildGraph(k KustomizationFileGetter, currentPath s
return nil
}

func (g *kustomizationGraph) addNodeToGraph(pathToAdd string, kustomizationFile *kustomizationfile.KustomizationFile) (string, error) {
func (g *kustomizationGraph) addNodeToGraph(pathToAdd string, missingResources []string) (string, error) {
node := sanitizePathForDot(pathToAdd)
if g.IsNode(node) {
return node, nil
}

nodeLabel := getNodeLabelFromMissingResources(pathToAdd, kustomizationFile.MissingResources)

nodeLabel := getNodeLabel(pathToAdd, missingResources)
err := g.AddNode(g.Name, node, nodeLabel)
if err != nil {
return "", errors.Wrapf(err, "Could not add node %s", node)
Expand All @@ -101,7 +114,7 @@ func (g *kustomizationGraph) addNodeToGraph(pathToAdd string, kustomizationFile
return node, nil
}

func getNodeLabelFromMissingResources(filePath string, missingResources []string) map[string]string {
func getNodeLabel(filePath string, missingResources []string) map[string]string {
missingResourcesLabel := make(map[string]string)
if len(missingResources) == 0 {
return missingResourcesLabel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package graph
package kustomizationgraph

import (
"testing"

"github.com/jpreese/kustomize-graph/pkg/kustomizationfile"
"github.com/spf13/afero"
"github.com/jpreese/kustomize-graph/pkg/kustomizationfile"
)

// TestGraph tests creating a graph using different ways
Expand Down Expand Up @@ -55,10 +55,12 @@ bases:
baseKustomizationFileContents := ""
afero.WriteFile(fakeFileSystem, "app/base/kustomization.yaml", []byte(baseKustomizationFileContents), 0644)

graph := NewGraph("main")
err = graph.buildGraph(kustomizationfile.ContextFromFileSystem(fakeFileSystem), "app", "")
graphContext := NewFromFileSystem(fakeFileSystem, "main")
kustomizationFileContext := kustomizationfile.NewFromFileSystem(fakeFileSystem)

err = graphContext.buildGraph(kustomizationFileContext, "app", "")
if err != nil {
t.Fatalf("Could not build graph %v.", err)
t.Fatalf("Could not generate graph %v.", err)
}

// Verify all of the expected nodes are present in the graph
Expand All @@ -69,21 +71,21 @@ bases:
wrapElement("app/base"),
}
for _, node := range expectedNodes {
if !graph.IsNode(node) {
if !graphContext.IsNode(node) {
t.Errorf("Expected node %v was not found", node)
}
}

// Verify all of the expected edges are present and their directions are correct
appFolderEdges := graph.Edges.SrcToDsts[wrapElement("app")]
appFolderEdges := graphContext.Edges.SrcToDsts[wrapElement("app")]
if _, exists := appFolderEdges[wrapElement("app/middle")]; !exists {
t.Errorf("Expected edge [app -> app/middle] was not found")
}
if _, exists := appFolderEdges[wrapElement("app/same")]; !exists {
t.Errorf("Expected edge [app -> app/same] was not found")
}

middleFolderEdges := graph.Edges.SrcToDsts[wrapElement("app/middle")]
middleFolderEdges := graphContext.Edges.SrcToDsts[wrapElement("app/middle")]
if _, exists := middleFolderEdges[wrapElement("app/base")]; !exists {
t.Errorf("Expected edge [app/middle -> app/base] was not found")
}
Expand Down

0 comments on commit 43c9b29

Please sign in to comment.