Skip to content

Commit

Permalink
add config option to exempt specific paths from pre-render branch cle…
Browse files Browse the repository at this point in the history
…aning (#212)

Signed-off-by: Kent <[email protected]>
Signed-off-by: Kent Rancourt <[email protected]>
  • Loading branch information
krancour authored Oct 26, 2023
1 parent a1b3303 commit 0c63b13
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 11 deletions.
82 changes: 72 additions & 10 deletions branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/ghodss/yaml"
"github.com/pkg/errors"
Expand Down Expand Up @@ -145,7 +146,10 @@ func switchToCommitBranch(rc requestContext) (string, error) {
}

// Clean the branch so we can replace its contents wholesale
if err := cleanCommitBranch(rc.repo.WorkingDir()); err != nil {
if err := cleanCommitBranch(
rc.repo.WorkingDir(),
rc.target.branchConfig.PreservedPaths,
); err != nil {
return "", errors.Wrap(err, "error cleaning commit branch")
}
logger.Debug("cleaned commit branch")
Expand All @@ -154,19 +158,77 @@ func switchToCommitBranch(rc requestContext) (string, error) {
}

// cleanCommitBranch deletes the entire contents of the specified directory
// EXCEPT for the .git and .kargo-render subdirectories.
func cleanCommitBranch(dir string) error {
dirEntries, err := os.ReadDir(dir)
// EXCEPT for the paths specified by preservedPaths.
func cleanCommitBranch(dir string, preservedPaths []string) error {
_, err := cleanDir(
dir,
normalizePreservedPaths(
dir,
append(preservedPaths, ".git", ".kargo-render"),
),
)
return err
}

// normalizePreservedPaths converts the relative paths in the preservedPaths
// argument to absolute paths relative to the workingDir argument. It also
// removes any trailing path separators from the paths.
func normalizePreservedPaths(
workingDir string,
preservedPaths []string,
) []string {
normalizedPreservedPaths := make([]string, len(preservedPaths))
for i, preservedPath := range preservedPaths {
if strings.HasSuffix(preservedPath, string(os.PathSeparator)) {
preservedPath = preservedPath[:len(preservedPath)-1]
}
normalizedPreservedPaths[i] = filepath.Join(workingDir, preservedPath)
}
return normalizedPreservedPaths
}

// cleanDir recursively deletes the entire contents of the directory specified
// by the absolute path dir EXCEPT for any paths specified by the preservedPaths
// argument. The function returns true if dir is left empty afterwards and false
// otherwise.
func cleanDir(dir string, preservedPaths []string) (bool, error) {
items, err := os.ReadDir(dir)
if err != nil {
return err
return false, err
}
for _, dirEntry := range dirEntries {
if dirEntry.Name() == ".git" || dirEntry.Name() == ".kargo-render" {
for _, item := range items {
path := filepath.Join(dir, item.Name())
if isPathPreserved(path, preservedPaths) {
continue
}
if err = os.RemoveAll(filepath.Join(dir, dirEntry.Name())); err != nil {
return err
if item.IsDir() {
var isEmpty bool
if isEmpty, err = cleanDir(path, preservedPaths); err != nil {
return false, err
}
if isEmpty {
if err = os.Remove(path); err != nil {
return false, err
}
}
} else if err = os.Remove(path); err != nil {
return false, err
}
}
return nil
if items, err = os.ReadDir(dir); err != nil {
return false, err
}
return len(items) == 0, nil
}

// isPathPreserved returns true if the specified path is among those specified
// by the preservedPaths argument. Both path and preservedPaths MUST be absolute
// paths. Paths to directories MUST NOT end with a trailing path separator.
func isPathPreserved(path string, preservedPaths []string) bool {
for _, preservedPath := range preservedPaths {
if path == preservedPath {
return true
}
}
return false
}
103 changes: 102 additions & 1 deletion branches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestCleanCommitBranch(t *testing.T) {
require.NoError(t, err)
require.Len(t, dirEntries, subdirCount+fileCount+2)
// Delete
err = cleanCommitBranch(dir)
err = cleanCommitBranch(dir, []string{})
require.NoError(t, err)
// .git should not have been deleted
_, err = os.Stat(filepath.Join(dir, ".git"))
Expand All @@ -121,6 +121,107 @@ func TestCleanCommitBranch(t *testing.T) {
require.Len(t, dirEntries, 2)
}

func TestNormalizePreservedPaths(t *testing.T) {
preservedPaths := []string{
"foo/bar",
"bat/baz/",
}
normalizedPreservedPaths :=
normalizePreservedPaths("fake-work-dir", preservedPaths)
require.Equal(
t,
[]string{
filepath.Join("fake-work-dir", "foo", "bar"),
filepath.Join("fake-work-dir", "bat", "baz"),
},
normalizedPreservedPaths,
)
}

func TestCleanDir(t *testing.T) {
dir, err := os.MkdirTemp("", "")
defer os.RemoveAll(dir)
require.NoError(t, err)

// This is what the test directory structure will look like:
// .
// ├── foo preserved directly
// │   └── foo.txt preserved because foo is
// ├── bar preserved because bar/bar.txt is
// │   └── bar.txt preserved directly
// ├── baz deleted because empty
// │   └── baz.txt deleted
// └── keep.txt preserved directly

// Create the test directory structure
fooDir := filepath.Join(dir, "foo")
err = os.Mkdir(fooDir, 0755)
require.NoError(t, err)
fooFile := filepath.Join(fooDir, "foo.txt")
err = os.WriteFile(fooFile, []byte("foo"), 0600)
require.NoError(t, err)

barDir := filepath.Join(dir, "bar")
err = os.Mkdir(barDir, 0755)
require.NoError(t, err)
barFile := filepath.Join(barDir, "bar.txt")
err = os.WriteFile(barFile, []byte("bar"), 0600)
require.NoError(t, err)

bazDir := filepath.Join(dir, "baz")
err = os.Mkdir(bazDir, 0755)
require.NoError(t, err)
bazFile := filepath.Join(bazDir, "baz.txt")
err = os.WriteFile(bazFile, []byte("baz"), 0600)
require.NoError(t, err)

keepFile := filepath.Join(dir, "keep.txt")
err = os.WriteFile(keepFile, []byte("keep"), 0600)
require.NoError(t, err)

preservedPaths := []string{
fooDir,
barFile,
keepFile,
}

isEmpty, err := cleanDir(dir, preservedPaths)
require.NoError(t, err)
require.False(t, isEmpty)

// Validate what was deleted and what wasn't

// All of foo/ remains
_, err = os.Stat(fooDir)
require.NoError(t, err)
_, err = os.Stat(fooFile)
require.NoError(t, err)

// All of bar/ remains
_, err = os.Stat(barDir)
require.NoError(t, err)
_, err = os.Stat(barFile)
require.NoError(t, err)

// All of baz/ is gone
_, err = os.Stat(bazDir)
require.True(t, os.IsNotExist(err))

// keep.txt remains
_, err = os.Stat(keepFile)
require.NoError(t, err)
}

func TestIsPathPreserved(t *testing.T) {
preservedPaths := []string{
"/foo/bar",
"/foo/bat",
}
require.True(t, isPathPreserved("/foo/bar", preservedPaths))
require.True(t, isPathPreserved("/foo/bat", preservedPaths))
require.False(t, isPathPreserved("/foo/baz", preservedPaths))
}

func createDummyCommitBranchDir(dirCount, fileCount int) (string, error) {
// Create a directory
dir, err := os.MkdirTemp("", "")
Expand Down
12 changes: 12 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ type branchConfig struct {
// PRs encapsulates details about how to manage any pull requests associated
// with this branch.
PRs pullRequestConfig `json:"prs,omitempty"`
// PreservedPaths specifies paths relative to the root of the repository that
// should be exempted from pre-render cleaning (deletion) of
// environment-specific branch contents. This is useful for preserving any
// environment-specific files that are manually maintained. Typically there
// are very few such files, if any at all, with an environment-specific
// CODEOWNERS file at the root of the repository being the most emblematic
// exception. Paths may be to files or directories. Any path to a directory
// will cause that directory's entire contents to be preserved.
PreservedPaths []string `json:"preservedPaths,omitempty"`
}

func (b branchConfig) expand(values []string) branchConfig {
Expand All @@ -77,6 +86,9 @@ func (b branchConfig) expand(values []string) branchConfig {
for appName, appConfig := range b.AppConfigs {
cfg.AppConfigs[appName] = appConfig.expand(values)
}
for i, path := range b.PreservedPaths {
b.PreservedPaths[i] = file.ExpandPath(path, values)
}
return cfg
}

Expand Down
6 changes: 6 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
},
"prs": {
"$ref": "#/definitions/pullRequestConfig"
},
"preservedPaths": {
"type": "array",
"items": {
"$ref": "#/definitions/relativePath"
}
}
}
},
Expand Down

0 comments on commit 0c63b13

Please sign in to comment.