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

feat: add WithFiles to Directory and Container #6556

Merged
merged 1 commit into from Feb 12, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Added-20240201-183804.yaml
@@ -0,0 +1,6 @@
kind: Added
body: add `Directory.WithFiles` and `Container.WithFiles`
time: 2024-02-01T18:38:04.314455612+01:00
custom:
Author: tomasmota
PR: "6556"
13 changes: 13 additions & 0 deletions core/container.go
Expand Up @@ -493,6 +493,19 @@ func (container *Container) WithFile(ctx context.Context, destPath string, src *
})
}

func (container *Container) WithFiles(ctx context.Context, destDir string, src []*File, permissions *int, owner string) (*Container, error) {
container = container.Clone()

return container.writeToPath(ctx, path.Dir(destDir), func(dir *Directory) (*Directory, error) {
ownership, err := container.ownership(ctx, owner)
if err != nil {
return nil, err
}

return dir.WithFiles(ctx, destDir, src, permissions, ownership)
})
}

func (container *Container) WithNewFile(ctx context.Context, dest string, content []byte, permissions fs.FileMode, owner string) (*Container, error) {
container = container.Clone()

Expand Down
27 changes: 27 additions & 0 deletions core/directory.go
Expand Up @@ -503,6 +503,33 @@ func (dir *Directory) WithFile(
return dir, nil
}

// TODO: address https://github.com/dagger/dagger/pull/6556/files#r1482830091
func (dir *Directory) WithFiles(
ctx context.Context,
destDir string,
src []*File,
permissions *int,
owner *Ownership,
) (*Directory, error) {
dir = dir.Clone()

var err error
for _, file := range src {
dir, err = dir.WithFile(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is fine today, I think we could potentially invert this - have WithFile call WithFiles.

We could have mergeStates take multiple source inputs, which would potentially be much more efficient (translating to one MergeOp instead of lots of individual ones). That said, this is a trickier optimization (and I think is also somewhat blocked on #6073).

Just a note, no action required here (but we could potentially leave a TODO comment for follow-up).

ctx,
path.Join(destDir, path.Base(file.File)),
file,
permissions,
owner,
)
if err != nil {
return nil, err
}
}

return dir, nil
}

type mergeStateInput struct {
Dest llb.State
DestDir string
Expand Down
28 changes: 28 additions & 0 deletions core/integration/container_test.go
Expand Up @@ -1766,6 +1766,34 @@ func TestContainerWithFile(t *testing.T) {
require.Equal(t, "some-content", contents)
}

func TestContainerWithFiles(t *testing.T) {
t.Parallel()

c, ctx := connect(t)

file1 := c.Directory().
WithNewFile("first-file", "file1 content").
File("first-file")
file2 := c.Directory().
WithNewFile("second-file", "file2 content").
File("second-file")
files := []*dagger.File{file1, file2}

ctr := c.Container().
From(alpineImage).
WithFiles("myfiles", files)

contents, err := ctr.WithExec([]string{"cat", "/myfiles/first-file"}).
Stdout(ctx)
require.NoError(t, err)
require.Equal(t, "file1 content", contents)

contents, err = ctr.WithExec([]string{"cat", "/myfiles/second-file"}).
Stdout(ctx)
require.NoError(t, err)
require.Equal(t, "file2 content", contents)
}

func TestContainerWithNewFile(t *testing.T) {
t.Parallel()

Expand Down
58 changes: 58 additions & 0 deletions core/integration/directory_test.go
Expand Up @@ -397,6 +397,64 @@ func TestDirectoryWithFile(t *testing.T) {
})
}

func TestDirectoryWithFiles(t *testing.T) {
t.Parallel()
c, ctx := connect(t)

file1 := c.Directory().
WithNewFile("first-file", "file1 content").
File("first-file")
file2 := c.Directory().
WithNewFile("second-file", "file2 content").
File("second-file")
files := []*dagger.File{file1, file2}
dir := c.Directory().
WithFiles("/", files)

// check file1 contents
content, err := dir.File("/first-file").Contents(ctx)
require.Equal(t, "file1 content", content)
require.NoError(t, err)

// check file2 contents
content, err = dir.File("/second-file").Contents(ctx)
require.Equal(t, "file2 content", content)
require.NoError(t, err)

_, err = dir.File("/some-other-file").Contents(ctx)
require.Error(t, err)

// test sub directory
subDir := c.Directory().
WithFiles("/a/b/c", files)
content, err = subDir.File("/a/b/c/first-file").Contents(ctx)
require.Equal(t, "file1 content", content)
require.NoError(t, err)

t.Run("respects permissions", func(t *testing.T) {
file1 := c.Directory().
WithNewFile("file-set-permissions", "this should have rwxrwxrwx permissions", dagger.DirectoryWithNewFileOpts{Permissions: 0o777}).
File("file-set-permissions")
file2 := c.Directory().
WithNewFile("file-default-permissions", "this should have rw-r--r-- permissions").
File("file-default-permissions")
files := []*dagger.File{file1, file2}
dir := c.Directory().
WithFiles("/", files)

ctr := c.Container().From("alpine").WithDirectory("/permissions-test", dir)

stdout, err := ctr.WithExec([]string{"ls", "-l", "/permissions-test/file-set-permissions"}).Stdout(ctx)
require.NoError(t, err)
require.Contains(t, stdout, "rwxrwxrwx")

ctr2 := c.Container().From("alpine").WithDirectory("/permissions-test", dir)
stdout2, err := ctr2.WithExec([]string{"ls", "-l", "/permissions-test/file-default-permissions"}).Stdout(ctx)
require.NoError(t, err)
require.Contains(t, stdout2, "rw-r--r--")
})
}

func TestDirectoryWithTimestamps(t *testing.T) {
t.Parallel()
c, ctx := connect(t)
Expand Down
28 changes: 28 additions & 0 deletions core/schema/container.go
Expand Up @@ -245,6 +245,16 @@ func (s *containerSchema) Install() {
`The user and group can either be an ID (1000:1000) or a name (foo:bar).`,
`If the group is omitted, it defaults to the same as the user.`),

dagql.Func("withFiles", s.withFiles).
Doc(`Retrieves this container plus the contents of the given files copied to the given path.`).
ArgDoc("path", `Location where copied files should be placed (e.g., "/src").`).
ArgDoc("sources", `Identifiers of the files to copy.`).
ArgDoc("permissions", `Permission given to the copied files (e.g., 0600).`).
ArgDoc("owner",
`A user:group to set for the files.`,
`The user and group can either be an ID (1000:1000) or a name (foo:bar).`,
`If the group is omitted, it defaults to the same as the user.`),

dagql.Func("withNewFile", s.withNewFile).
Doc(`Retrieves this container plus a new file written at the given path.`).
ArgDoc("path", `Location of the written file (e.g., "/tmp/file.txt").`).
Expand Down Expand Up @@ -1062,6 +1072,24 @@ func (s *containerSchema) withFile(ctx context.Context, parent *core.Container,
return parent.WithFile(ctx, args.Path, file.Self, args.Permissions, args.Owner)
}

type containerWithFilesArgs struct {
WithFilesArgs
Owner string `default:""`
}

func (s *containerSchema) withFiles(ctx context.Context, parent *core.Container, args containerWithFilesArgs) (*core.Container, error) {
files := []*core.File{}
for _, id := range args.Sources {
file, err := id.Load(ctx, s.srv)
if err != nil {
return nil, err
}
files = append(files, file.Self)
}

return parent.WithFiles(ctx, args.Path, files, args.Permissions, args.Owner)
}

type containerWithNewFileArgs struct {
Path string
Contents string `default:""`
Expand Down
24 changes: 24 additions & 0 deletions core/schema/directory.go
Expand Up @@ -45,6 +45,11 @@ func (s *directorySchema) Install() {
ArgDoc("path", `Location of the copied file (e.g., "/file.txt").`).
ArgDoc("source", `Identifier of the file to copy.`).
ArgDoc("permissions", `Permission given to the copied file (e.g., 0600).`),
dagql.Func("withFiles", s.withFiles).
Doc(`Retrieves this directory plus the contents of the given files copied to the given path.`).
ArgDoc("path", `Location where copied files should be placed (e.g., "/src").`).
ArgDoc("sources", `Identifiers of the files to copy.`).
ArgDoc("permissions", `Permission given to the copied files (e.g., 0600).`),
dagql.Func("withNewFile", s.withNewFile).
Doc(`Retrieves this directory plus a new file written at the given path.`).
ArgDoc("path", `Location of the written file (e.g., "/file.txt").`).
Expand Down Expand Up @@ -212,6 +217,25 @@ func (s *directorySchema) withFile(ctx context.Context, parent *core.Directory,
return parent.WithFile(ctx, args.Path, file.Self, args.Permissions, nil)
}

type WithFilesArgs struct {
Path string
Sources []core.FileID
Permissions *int
}

func (s *directorySchema) withFiles(ctx context.Context, parent *core.Directory, args WithFilesArgs) (*core.Directory, error) {
files := []*core.File{}
for _, id := range args.Sources {
file, err := id.Load(ctx, s.srv)
if err != nil {
return nil, err
}
files = append(files, file.Self)
}

return parent.WithFiles(ctx, args.Path, files, args.Permissions, nil)
}

type withoutDirectoryArgs struct {
Path string
}
Expand Down
37 changes: 37 additions & 0 deletions docs/docs-graphql/schema.graphqls
Expand Up @@ -514,6 +514,29 @@ type Container {
source: FileID!
): Container!

"""
Retrieves this container plus the contents of the given files copied to the given path.
"""
withFiles(
"""
A user:group to set for the files.

The user and group can either be an ID (1000:1000) or a name (foo:bar).

If the group is omitted, it defaults to the same as the user.
"""
owner: String = ""

"""Location where copied files should be placed (e.g., "/src")."""
path: String!

"""Permission given to the copied files (e.g., 0600)."""
permissions: Int

"""Identifiers of the files to copy."""
sources: [FileID!]!
): Container!

"""
Indicate that subsequent operations should be featured more prominently in the UI.
"""
Expand Down Expand Up @@ -1000,6 +1023,20 @@ type Directory {
source: FileID!
): Directory!

"""
Retrieves this directory plus the contents of the given files copied to the given path.
"""
withFiles(
"""Location where copied files should be placed (e.g., "/src")."""
path: String!

"""Permission given to the copied files (e.g., 0600)."""
permissions: Int

"""Identifiers of the files to copy."""
sources: [FileID!]!
): Directory!

"""
Retrieves this directory plus a new directory created at the given path.
"""
Expand Down
27 changes: 27 additions & 0 deletions sdk/elixir/lib/dagger/gen/container.ex

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions sdk/elixir/lib/dagger/gen/directory.ex

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.