Set the write bit for files written during template initialization (#2068)

## Changes

This used to work because the permission bits for built-in templates
were hardcoded to 0644 for files and 0755 for directories.

As of #1912 (and the PRs it depends on), built-in templates are no
longer pre-materialized to a temporary directory and read directly from
the embedded filesystem. This built-in filesystem returns 0444 as the
permission bits for the files it contains. These bits are carried over
to the destination filesystem.

This change updates template materialization to always set the owner's
write bit. It doesn't really make sense to write read-only files and
expect users to work with these files in a VCS (note: Git only stores
the executable bit).

The regression shipped as part of v0.235.0 and will be fixed as of
v0.238.0.

## Tests

Unit tests.
This commit is contained in:
Pieter Noordhuis 2025-01-08 14:18:28 +01:00 committed by GitHub
parent 0289becea8
commit 23f05f5d67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 60 additions and 19 deletions

View File

@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -52,3 +53,31 @@ func ReadFile(t TestingT, path string) string {
return string(b)
}
// StatFile returns the file info for a file.
func StatFile(t TestingT, path string) os.FileInfo {
fi, err := os.Stat(path)
require.NoError(t, err)
return fi
}
// AssertFileContents asserts that the file at path has the expected content.
func AssertFileContents(t TestingT, path, expected string) bool {
actual := ReadFile(t, path)
return assert.Equal(t, expected, actual)
}
// AssertFilePermissions asserts that the file at path has the expected permissions.
func AssertFilePermissions(t TestingT, path string, expected os.FileMode) bool {
fi := StatFile(t, path)
assert.False(t, fi.Mode().IsDir(), "expected a file, got a directory")
return assert.Equal(t, expected, fi.Mode().Perm(), "expected 0%o, got 0%o", expected, fi.Mode().Perm())
}
// AssertDirPermissions asserts that the file at path has the expected permissions.
func AssertDirPermissions(t TestingT, path string, expected os.FileMode) bool {
fi := StatFile(t, path)
assert.True(t, fi.Mode().IsDir(), "expected a directory, got a file")
return assert.Equal(t, expected, fi.Mode().Perm(), "expected 0%o, got 0%o", expected, fi.Mode().Perm())
}

View File

@ -8,6 +8,7 @@ import (
"runtime"
"testing"
"github.com/databricks/cli/internal/testutil"
"github.com/databricks/cli/libs/filer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -27,8 +28,8 @@ func testInMemoryFile(t *testing.T, ctx context.Context, perm fs.FileMode) {
err = f.Write(ctx, out)
assert.NoError(t, err)
assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "123")
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm)
testutil.AssertFileContents(t, filepath.Join(tmpDir, "a/b/c"), "123")
testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm)
}
func testCopyFile(t *testing.T, ctx context.Context, perm fs.FileMode) {
@ -48,8 +49,8 @@ func testCopyFile(t *testing.T, ctx context.Context, perm fs.FileMode) {
err = f.Write(ctx, out)
assert.NoError(t, err)
assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "qwerty")
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm)
testutil.AssertFileContents(t, filepath.Join(tmpDir, "source"), "qwerty")
testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "source"), perm)
}
func TestTemplateInMemoryFilePersistToDisk(t *testing.T) {

View File

@ -150,6 +150,10 @@ func (r *renderer) computeFile(relPathTemplate string) (file, error) {
}
perm := info.Mode().Perm()
// Always include the write bit for the owner of the file.
// It does not make sense to have a file that is not writable by the owner.
perm |= 0o200
// Execute relative path template to get destination path for the file
relPath, err := r.executeTemplate(relPathTemplate)
if err != nil {

View File

@ -27,16 +27,19 @@ import (
"github.com/stretchr/testify/require"
)
func assertFileContent(t *testing.T, path, content string) {
b, err := os.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, content, string(b))
}
var (
defaultFilePermissions fs.FileMode
defaultDirPermissions fs.FileMode
)
func assertFilePermissions(t *testing.T, path string, perm fs.FileMode) {
info, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, perm, info.Mode().Perm())
func init() {
if runtime.GOOS == "windows" {
defaultFilePermissions = fs.FileMode(0o666)
defaultDirPermissions = fs.FileMode(0o777)
} else {
defaultFilePermissions = fs.FileMode(0o644)
defaultDirPermissions = fs.FileMode(0o755)
}
}
func assertBuiltinTemplateValid(t *testing.T, template string, settings map[string]any, target string, isServicePrincipal, build bool, tempDir string) {
@ -69,6 +72,10 @@ func assertBuiltinTemplateValid(t *testing.T, template string, settings map[stri
err = renderer.persistToDisk(ctx, out)
require.NoError(t, err)
// Verify permissions on file and directory
testutil.AssertFilePermissions(t, filepath.Join(tempDir, "my_project/README.md"), defaultFilePermissions)
testutil.AssertDirPermissions(t, filepath.Join(tempDir, "my_project/resources"), defaultDirPermissions)
b, err := bundle.Load(ctx, filepath.Join(tempDir, "my_project"))
require.NoError(t, err)
diags := bundle.Apply(ctx, b, phases.LoadNamedTarget(target))
@ -347,10 +354,10 @@ func TestRendererPersistToDisk(t *testing.T) {
assert.NoFileExists(t, filepath.Join(tmpDir, "a", "b", "c"))
assert.NoFileExists(t, filepath.Join(tmpDir, "mno"))
assertFileContent(t, filepath.Join(tmpDir, "a", "b", "d"), "123")
assertFilePermissions(t, filepath.Join(tmpDir, "a", "b", "d"), 0o444)
assertFileContent(t, filepath.Join(tmpDir, "mmnn"), "456")
assertFilePermissions(t, filepath.Join(tmpDir, "mmnn"), 0o444)
testutil.AssertFileContents(t, filepath.Join(tmpDir, "a/b/d"), "123")
testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "a/b/d"), fs.FileMode(0o444))
testutil.AssertFileContents(t, filepath.Join(tmpDir, "mmnn"), "456")
testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "mmnn"), fs.FileMode(0o444))
}
func TestRendererWalk(t *testing.T) {
@ -617,8 +624,8 @@ func TestRendererFileTreeRendering(t *testing.T) {
require.NoError(t, err)
// Assert files and directories are correctly materialized.
assert.DirExists(t, filepath.Join(tmpDir, "my_directory"))
assert.FileExists(t, filepath.Join(tmpDir, "my_directory", "my_file"))
testutil.AssertDirPermissions(t, filepath.Join(tmpDir, "my_directory"), defaultDirPermissions)
testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "my_directory", "my_file"), defaultFilePermissions)
}
func TestRendererSubTemplateInPath(t *testing.T) {