diff --git a/examples/buildexample/.yeyrc.yaml b/examples/buildexample/.yeyrc.yaml new file mode 100644 index 0000000..40b6bb4 --- /dev/null +++ b/examples/buildexample/.yeyrc.yaml @@ -0,0 +1,3 @@ +build: + dockerfile: ./Dockerfile + context: . diff --git a/examples/buildexample/Dockerfile b/examples/buildexample/Dockerfile new file mode 100644 index 0000000..254b321 --- /dev/null +++ b/examples/buildexample/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu:latest + +CMD ["bash"] \ No newline at end of file diff --git a/go.mod b/go.mod index 6e8c5ff..24feb09 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/silphid/yey -go 1.14 +go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.2.12 diff --git a/go.sum b/go.sum index 8b6bfe7..ca68029 100644 --- a/go.sum +++ b/go.sum @@ -178,6 +178,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/src/cmd/run/run.go b/src/cmd/run/run.go index 51cdefc..633bbff 100644 --- a/src/cmd/run/run.go +++ b/src/cmd/run/run.go @@ -3,9 +3,11 @@ package run import ( "context" "fmt" + "os" yey "github.com/silphid/yey/src/internal" "github.com/silphid/yey/src/internal/docker" + "github.com/silphid/yey/src/internal/logging" "github.com/silphid/yey/src/cmd" @@ -65,6 +67,15 @@ func run(ctx context.Context, name string, options Options) error { yeyContext.Remove = options.Remove } + if yeyContext.Image == "" { + var err error + yeyContext.Image, err = readAndBuildDockerfile(ctx, yeyContext.Build) + if err != nil { + return fmt.Errorf("failed to build yey context image: %w", err) + } + logging.Log("built image: %s", yeyContext.Image) + } + containerName := yey.ContainerName(contexts.Path, yeyContext) if options.Reset { @@ -75,3 +86,18 @@ func run(ctx context.Context, name string, options Options) error { return docker.Start(ctx, yeyContext, containerName) } + +func readAndBuildDockerfile(ctx context.Context, build yey.DockerBuild) (string, error) { + dockerBytes, err := os.ReadFile(build.Dockerfile) + if err != nil { + return "", fmt.Errorf("failed to read dockerfile: %w", err) + } + + imageName := yey.ImageName(dockerBytes) + + if err := docker.Build(ctx, build.Dockerfile, imageName, build.Args, build.Context); err != nil { + return "", fmt.Errorf("failed to build image: %w", err) + } + + return imageName, nil +} diff --git a/src/internal/context.go b/src/internal/context.go index df30cc2..9cff009 100644 --- a/src/internal/context.go +++ b/src/internal/context.go @@ -4,11 +4,18 @@ import ( "gopkg.in/yaml.v2" ) +type DockerBuild struct { + Dockerfile string + Args map[string]string + Context string +} + // Context represents execution configuration for some docker container type Context struct { Name string `yaml:",omitempty"` Remove *bool Image string + Build DockerBuild Env map[string]string Mounts map[string]string Cmd []string @@ -30,6 +37,10 @@ func (c Context) Clone() Context { for key, value := range c.Mounts { clone.Mounts[key] = value } + clone.Build.Args = make(map[string]string) + for key, value := range c.Build.Args { + clone.Build.Args[key] = value + } return clone } @@ -49,6 +60,12 @@ func (c Context) Merge(source Context) Context { for key, value := range source.Mounts { merged.Mounts[key] = value } + if source.Build.Dockerfile != "" { + merged.Build.Dockerfile = source.Build.Dockerfile + } + for key, value := range source.Build.Args { + merged.Build.Args[key] = value + } return merged } diff --git a/src/internal/contextFile.go b/src/internal/contextFile.go index 78c4f72..0432671 100644 --- a/src/internal/contextFile.go +++ b/src/internal/contextFile.go @@ -74,7 +74,7 @@ func readContextFileFromNetwork(url string) ([]byte, error) { } // parseContextFile unmarshals the contextFile data and resolves any parent contextfiles -func parseContextFile(data []byte) (Contexts, error) { +func parseContextFile(root string, data []byte) (Contexts, error) { var ctxFile ContextFile if err := yaml.Unmarshal(data, &ctxFile); err != nil { return Contexts{}, fmt.Errorf("failed to decode context file: %w", err) @@ -89,6 +89,13 @@ func parseContextFile(data []byte) (Contexts, error) { Named: ctxFile.Named, } + if root != "" { + contexts.Context = resolveContextPaths(root, contexts.Context) + for name, context := range contexts.Named { + contexts.Named[name] = resolveContextPaths(root, context) + } + } + if ctxFile.Parent != "" { parent, err := readAndParseContextFileFromURI(ctxFile.Parent) if err != nil { @@ -105,10 +112,12 @@ func parseContextFile(data []byte) (Contexts, error) { func readAndParseContextFileFromURI(path string) (Contexts, error) { var bytes []byte var err error + var root string if strings.HasPrefix(path, "https:") || strings.HasPrefix(path, "http:") { bytes, err = readContextFileFromNetwork(path) } else { + root = filepath.Dir(path) bytes, err = readContextFileFromFilePath(path) } @@ -116,7 +125,7 @@ func readAndParseContextFileFromURI(path string) (Contexts, error) { return Contexts{}, fmt.Errorf("failed to read context file: %w", err) } - return parseContextFile(bytes) + return parseContextFile(root, bytes) } // LoadContexts reads the context file and returns the contexts. It starts by reading from current @@ -127,7 +136,7 @@ func LoadContexts() (Contexts, error) { return Contexts{}, fmt.Errorf("failed to read context file: %w", err) } - contexts, err := parseContextFile(bytes) + contexts, err := parseContextFile(filepath.Dir(path), bytes) if err != nil { return Contexts{}, err } @@ -135,3 +144,23 @@ func LoadContexts() (Contexts, error) { return contexts, nil } + +func resolveContextPaths(root string, context Context) Context { + clone := context.Clone() + clone.Build.Dockerfile = resolvePath(root, context.Build.Dockerfile) + clone.Build.Context = resolvePath(root, clone.Build.Context) + for key, value := range clone.Mounts { + clone.Mounts[resolvePath(root, key)] = value + } + return clone +} + +func resolvePath(root, path string) string { + if path == "" { + return "" + } + if strings.HasPrefix(path, "/") { + return path + } + return filepath.Join(root, path) +} diff --git a/src/internal/docker/cli.go b/src/internal/docker/cli.go index b91ab07..0923175 100644 --- a/src/internal/docker/cli.go +++ b/src/internal/docker/cli.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "regexp" "strings" @@ -45,6 +46,19 @@ func Remove(ctx context.Context, containerName string) error { return attachStdPipes(exec.CommandContext(ctx, "docker", "rm", "-v", containerName)).Run() } +func Build(ctx context.Context, dockerPath string, imageName string, buildArgs map[string]string, context string) error { + args := []string{"build", "-f", dockerPath, "-t", imageName} + for key, value := range buildArgs { + args = append(args, "--build-arg", fmt.Sprintf("%s=%q", key, value)) + } + if context == "" { + context = filepath.Dir(dockerPath) + } + args = append(args, context) + + return attachStdPipes(exec.CommandContext(ctx, "docker", args...)).Run() +} + var newlines = regexp.MustCompile(`\r?\n`) func ListContainers(ctx context.Context) ([]string, error) { diff --git a/src/internal/yey.go b/src/internal/yey.go index e502c2f..42f4da8 100644 --- a/src/internal/yey.go +++ b/src/internal/yey.go @@ -56,3 +56,7 @@ func ContainerPathPrefix(path string) string { } return fmt.Sprintf("yey-%s-%s", pathBase, hash(path)) } + +func ImageName(dockerfile []byte) string { + return fmt.Sprintf("yey-%s", hash(string(dockerfile))) +}