Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add config option to exempt specific paths from pre-render branch cleaning #212

Merged
merged 2 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 72 additions & 10 deletions branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"fmt"
"os"
"path/filepath"
"strings"

"github.com/ghodss/yaml"
"github.com/pkg/errors"
Expand Down Expand Up @@ -145,7 +146,10 @@
}

// 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 {

Check warning on line 152 in branches.go

View check run for this annotation

Codecov / codecov/patch

branches.go#L149-L152

Added lines #L149 - L152 were not covered by tests
return "", errors.Wrap(err, "error cleaning commit branch")
}
logger.Debug("cleaned commit branch")
Expand All @@ -154,19 +158,77 @@
}

// 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

Check warning on line 197 in branches.go

View check run for this annotation

Codecov / codecov/patch

branches.go#L197

Added line #L197 was not covered by tests
}
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
}

Check warning on line 208 in branches.go

View check run for this annotation

Codecov / codecov/patch

branches.go#L207-L208

Added lines #L207 - L208 were not covered by tests
if isEmpty {
if err = os.Remove(path); err != nil {
return false, err
}

Check warning on line 212 in branches.go

View check run for this annotation

Codecov / codecov/patch

branches.go#L211-L212

Added lines #L211 - L212 were not covered by tests
}
} else if err = os.Remove(path); err != nil {
return false, err

Check warning on line 215 in branches.go

View check run for this annotation

Codecov / codecov/patch

branches.go#L215

Added line #L215 was not covered by tests
}
}
return nil
if items, err = os.ReadDir(dir); err != nil {
return false, err
}

Check warning on line 220 in branches.go

View check run for this annotation

Codecov / codecov/patch

branches.go#L219-L220

Added lines #L219 - L220 were not covered by tests
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 @@
// 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 @@
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)
}

Check warning on line 91 in config.go

View check run for this annotation

Codecov / codecov/patch

config.go#L90-L91

Added lines #L90 - L91 were not covered by tests
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