package cli import ( "errors" "flag" "fmt" "io" "os" "path/filepath" "werkdock/internal/build" "werkdock/internal/registry" "werkdock/internal/store" ) // buildOptions is the parsed form of `werkdock build` flags, // separated from execution so the parsing is testable (as parseRun is). type buildOptions struct { Dockerfile string Context string Tag string Quiet bool NoCache bool } // buildCmd is `docker build`: an image from a Dockerfile, built on this // host (RFC 0006). `werkdock build [-f Dockerfile] -t IMAGE CONTEXT`. func buildCmd(args []string) int { opts, err := parseBuild(args) if err != nil { return fail(err) } st, err := store.Default() if err != nil { return fail(err) } backend, how := build.Select() log := io.Writer(os.Stdout) if opts.Quiet { log = io.Discard } fmt.Fprintf(log, "werkdock: %s\n", how) if opts.NoCache && !backend.CachesSteps() { fmt.Fprintf(log, "werkdock: --no-cache is a no-op here; this backend caches no steps\n") } result, err := build.Run(st, build.Options{ Dockerfile: opts.Dockerfile, Context: opts.Context, Tag: opts.Tag, Backend: backend, Resolve: resolveBase(st), Stdout: log, Stderr: os.Stderr, }) if err != nil { return fail(err) } if opts.Quiet { fmt.Println(result.ID) } return 0 } // parseBuild parses the docker-shaped build flags. Docker flags whose // promise werkdock cannot keep are registered and refused with a // reason — never silently ignored (RFC 0002). func parseBuild(args []string) (*buildOptions, error) { fs := flag.NewFlagSet("build", flag.ContinueOnError) fs.SetOutput(io.Discard) var tags stringList dockerfile := fs.String("f", "", "Dockerfile to read (default CONTEXT/Dockerfile)") fs.StringVar(dockerfile, "file", "", "Dockerfile to read (default CONTEXT/Dockerfile)") fs.Var(&tags, "t", "reference for the built image, REPOSITORY[:TAG]") fs.Var(&tags, "tag", "reference for the built image, REPOSITORY[:TAG]") quiet := fs.Bool("q", false, "print the image id only") fs.BoolVar(quiet, "quiet", false, "print the image id only") // docker's --no-cache asks for what the copy backend does anyway. noCache := fs.Bool("no-cache", false, "do not use a build cache (the copy backend has none)") refuse(fs, "build-arg", "ARG and --build-arg are stage 2 of RFC 0006") refuse(fs, "target", "--target needs multi-stage builds, which are stage 3 of RFC 0006") refuse(fs, "platform", "werkdock builds for the host's platform; there is no emulation") refuse(fs, "pull", "the base image is pulled when the store does not have it; werkdock rmi it to force a refetch") refuse(fs, "squash", "a werkdock image is one flat tree already; there is nothing to squash") refuse(fs, "cache-from", "there is no layer cache to import from (RFC 0006, What \"no layers\" means)") refuse(fs, "cache-to", "there is no layer cache to export (RFC 0006, What \"no layers\" means)") refuse(fs, "secret", "build secrets are BuildKit's; a RUN step cannot mount anything (CapEff is 0 in the sandbox)") refuse(fs, "ssh", "SSH agent forwarding into a build is BuildKit's") refuse(fs, "network", "the sandbox shares the host's network by contract; there is no build network to choose") refuse(fs, "output", "--output is BuildKit's exporter; the result is a store image, and save writes an archive") refuse(fs, "load", "--load is buildx's; the image is in the store when the build succeeded") refuse(fs, "push", "--push is buildx's; werkdock has no push verb") refuse(fs, "progress", "werkdock prints the steps and the commands' own output, nothing else") if err := fs.Parse(args); err != nil { return nil, err } rest := fs.Args() if len(rest) == 0 { return nil, errors.New("build needs a CONTEXT directory (werkdock build [-f Dockerfile] -t IMAGE CONTEXT)") } if len(rest) > 1 { return nil, fmt.Errorf("unexpected argument %q", rest[1]) } if rest[0] == "-" { return nil, errors.New("a Dockerfile on stdin needs no context and is not supported; pass a context directory") } if len(tags) == 0 { return nil, errors.New("build needs -t REPOSITORY[:TAG]: a werkdock image is named, there is no untagged image") } if len(tags) > 1 { return nil, errors.New("one -t per build: a second reference for the same image needs a tag verb, which werkdock does not have yet") } if _, err := store.ParseReference(tags[0]); err != nil { return nil, err } opts := &buildOptions{Context: rest[0], Tag: tags[0], Quiet: *quiet, NoCache: *noCache, Dockerfile: *dockerfile} if opts.Dockerfile == "" { opts.Dockerfile = filepath.Join(opts.Context, "Dockerfile") } return opts, nil } // resolveBase is FROM's lookup: the store first, then a pull, as // `docker build` pulls a base it does not have. Without a registry // client the error names the two verbs that would put the image there. func resolveBase(st store.Store) func(string) (string, store.ImageMeta, error) { return func(reference string) (string, store.ImageMeta, error) { rootfs, err := st.RootFS(reference) if err == nil { meta, metaErr := st.Meta(reference) return rootfs, meta, metaErr } fmt.Fprintf(os.Stderr, "werkdock: %s is not in the store, pulling it\n", reference) if _, pullErr := registry.Pull(st, reference, registry.DefaultPlatform(), os.Stderr); pullErr != nil { return "", store.ImageMeta{}, fmt.Errorf("FROM %s: not in the store and could not be pulled: %w\n"+ "\tput it there with: werkdock pull %s — or werkdock load -i ARCHIVE", reference, pullErr, reference) } rootfs, err = st.RootFS(reference) if err != nil { return "", store.ImageMeta{}, err } meta, err := st.Meta(reference) return rootfs, meta, err } }