Skip to content

Commit

Permalink
Make FROM scratch a special cased 'no-base' spec
Browse files Browse the repository at this point in the history
There has been a lot of discussion (issues 4242 and 5262) about making
`FROM scratch` either a special case or making `FROM` optional, implying
starting from an empty file system.

This patch makes the build command `FROM scratch` special cased from now on
and if used does not pull/set the the initial layer of the build to the ancient
image ID (511136ea..) but instead marks the build as having no base image. The
next command in the dockerfile will create an image with a parent image ID of "".
This means every image ever can now use one fewer layer!

This also makes the image name `scratch` a reserved name by the TagStore. You
will not be able to tag an image with this name from now on. If any users
currently have an image tagged as `scratch`, they will still be able to use that
image, but will not be able to tag a new image with that name.

Goodbye '511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158',
it was nice knowing you.

Fixes moby#4242

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
  • Loading branch information
Josh Hawn authored and Colin committed Jan 5, 2015
1 parent 78594d0 commit 9673beb
Show file tree
Hide file tree
Showing 17 changed files with 70 additions and 42 deletions.
7 changes: 6 additions & 1 deletion api/client/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1758,7 +1758,12 @@ func (cli *DockerCli) CmdPs(args ...string) error {

ports.ReadListFrom([]byte(out.Get("Ports")))

fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", outID, out.Get("Image"), outCommand,
image := out.Get("Image")
if image == "" {
image = "<no image>"
}

fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", outID, image, outCommand,
units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))),
out.Get("Status"), api.DisplayablePorts(ports), strings.Join(outNames, ","))

Expand Down
14 changes: 13 additions & 1 deletion builder/dispatchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
"github.com/docker/docker/runconfig"
)

const (
// NoBaseImageSpecifier is the symbol used by the FROM
// command to specify that no base image is to be used.
NoBaseImageSpecifier string = "scratch"
)

// dispatch with no layer / parsing. This is effectively not a command.
func nullDispatch(b *Builder, args []string, attributes map[string]bool, original string) error {
return nil
Expand Down Expand Up @@ -115,6 +121,12 @@ func from(b *Builder, args []string, attributes map[string]bool, original string

name := args[0]

if name == NoBaseImageSpecifier {
b.image = ""
b.noBaseImage = true
return nil
}

image, err := b.Daemon.Repositories().LookupImage(name)
if b.Pull {
image, err = b.pullImage(name)
Expand Down Expand Up @@ -191,7 +203,7 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str
// RUN [ "echo", "hi" ] # echo hi
//
func run(b *Builder, args []string, attributes map[string]bool, original string) error {
if b.image == "" {
if b.image == "" && !b.noBaseImage {
return fmt.Errorf("Please provide a source image with `from` prior to run")
}

Expand Down
2 changes: 1 addition & 1 deletion builder/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ type Builder struct {
cmdSet bool // indicates is CMD was set in current Dockerfile
context tarsum.TarSum // the context is a tarball that is uploaded by the client
contextPath string // the path of the temporary directory the local context is unpacked to (server side)

noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system.
}

// Run the builder with the context. This is the lynchpin of this package. This
Expand Down
4 changes: 2 additions & 2 deletions builder/internals.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (b *Builder) readContext(context io.Reader) error {
}

func (b *Builder) commit(id string, autoCmd []string, comment string) error {
if b.image == "" {
if b.image == "" && !b.noBaseImage {
return fmt.Errorf("Please provide a source image with `from` prior to commit")
}
b.Config.Image = b.image
Expand Down Expand Up @@ -513,7 +513,7 @@ func (b *Builder) probeCache() (bool, error) {
}

func (b *Builder) create() (*daemon.Container, error) {
if b.image == "" {
if b.image == "" && !b.noBaseImage {
return nil, fmt.Errorf("Please provide a source image with `from` prior to run")
}
b.Config.Image = b.image
Expand Down
8 changes: 4 additions & 4 deletions daemon/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ func (daemon *Daemon) Commit(container *Container, repository, tag, comment, aut

// Create a new image from the container's base layers + a new layer from container changes
var (
containerID, containerImage string
containerConfig *runconfig.Config
containerID, parentImageID string
containerConfig *runconfig.Config
)

if container != nil {
containerID = container.ID
containerImage = container.Image
parentImageID = container.ImageID
containerConfig = container.Config
}

img, err := daemon.graph.Create(rwTar, containerID, containerImage, comment, author, containerConfig, config)
img, err := daemon.graph.Create(rwTar, containerID, parentImageID, comment, author, containerConfig, config)
if err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions daemon/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ type Container struct {
Path string
Args []string

Config *runconfig.Config
Image string
Config *runconfig.Config
ImageID string `json:"Image"`

NetworkSettings *NetworkSettings

Expand Down Expand Up @@ -186,7 +186,7 @@ func (container *Container) WriteHostConfig() error {

func (container *Container) LogEvent(action string) {
d := container.daemon
if err := d.eng.Job("log", action, container.ID, d.Repositories().ImageName(container.Image)).Run(); err != nil {
if err := d.eng.Job("log", action, container.ID, d.Repositories().ImageName(container.ImageID)).Run(); err != nil {
log.Errorf("Error logging event %s for %s: %s", action, container.ID, err)
}
}
Expand Down Expand Up @@ -786,7 +786,7 @@ func (container *Container) GetImage() (*image.Image, error) {
if container.daemon == nil {
return nil, fmt.Errorf("Can't get image of unregistered container")
}
return container.daemon.graph.Get(container.Image)
return container.daemon.graph.Get(container.ImageID)
}

func (container *Container) Unmount() error {
Expand Down
24 changes: 16 additions & 8 deletions daemon/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/docker/docker/engine"
"github.com/docker/docker/graph"
"github.com/docker/docker/image"
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/runconfig"
"github.com/docker/libcontainer/label"
Expand Down Expand Up @@ -68,15 +69,22 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos
var (
container *Container
warnings []string
img *image.Image
imgID string
err error
)

img, err := daemon.repositories.LookupImage(config.Image)
if err != nil {
return nil, nil, err
}
if err := img.CheckDepth(); err != nil {
return nil, nil, err
if config.Image != "" {
img, err = daemon.repositories.LookupImage(config.Image)
if err != nil {
return nil, nil, err
}
if err = img.CheckDepth(); err != nil {
return nil, nil, err
}
imgID = img.ID
}

if warnings, err = daemon.mergeAndVerifyConfig(config, img); err != nil {
return nil, nil, err
}
Expand All @@ -86,13 +94,13 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos
return nil, nil, err
}
}
if container, err = daemon.newContainer(name, config, img); err != nil {
if container, err = daemon.newContainer(name, config, imgID); err != nil {
return nil, nil, err
}
if err := daemon.Register(container); err != nil {
return nil, nil, err
}
if err := daemon.createRootfs(container, img); err != nil {
if err := daemon.createRootfs(container); err != nil {
return nil, nil, err
}
if hostConfig != nil {
Expand Down
12 changes: 6 additions & 6 deletions daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,10 +417,10 @@ func (daemon *Daemon) checkDeprecatedExpose(config *runconfig.Config) bool {

func (daemon *Daemon) mergeAndVerifyConfig(config *runconfig.Config, img *image.Image) ([]string, error) {
warnings := []string{}
if daemon.checkDeprecatedExpose(img.Config) || daemon.checkDeprecatedExpose(config) {
if (img != nil && daemon.checkDeprecatedExpose(img.Config)) || daemon.checkDeprecatedExpose(config) {
warnings = append(warnings, "The mapping to public ports on your host via Dockerfile EXPOSE (host:port:port) has been deprecated. Use -p to publish the ports.")
}
if img.Config != nil {
if img != nil && img.Config != nil {
if err := runconfig.Merge(config, img.Config); err != nil {
return nil, err
}
Expand Down Expand Up @@ -557,7 +557,7 @@ func parseSecurityOpt(container *Container, config *runconfig.HostConfig) error
return err
}

func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *image.Image) (*Container, error) {
func (daemon *Daemon) newContainer(name string, config *runconfig.Config, imgID string) (*Container, error) {
var (
id string
err error
Expand All @@ -578,7 +578,7 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i
Args: args, //FIXME: de-duplicate from config
Config: config,
hostConfig: &runconfig.HostConfig{},
Image: img.ID, // Always use the resolved image id
ImageID: imgID,
NetworkSettings: &NetworkSettings{},
Name: name,
Driver: daemon.driver.String(),
Expand All @@ -590,14 +590,14 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i
return container, err
}

func (daemon *Daemon) createRootfs(container *Container, img *image.Image) error {
func (daemon *Daemon) createRootfs(container *Container) error {
// Step 1: create the container directory.
// This doubles as a barrier to avoid race conditions.
if err := os.Mkdir(container.root, 0700); err != nil {
return err
}
initID := fmt.Sprintf("%s-init", container.ID)
if err := daemon.driver.Create(initID, img.ID); err != nil {
if err := daemon.driver.Create(initID, container.ImageID); err != nil {
return err
}
initPath, err := daemon.driver.Get(initID, "")
Expand Down
2 changes: 1 addition & 1 deletion daemon/image_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine.

func (daemon *Daemon) canDeleteImage(imgID string, force bool) error {
for _, container := range daemon.List() {
parent, err := daemon.Repositories().LookupImage(container.Image)
parent, err := daemon.Repositories().LookupImage(container.ImageID)
if err != nil {
if daemon.Graph().IsNotExist(err) {
return nil
Expand Down
2 changes: 1 addition & 1 deletion daemon/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status {
out.SetList("Args", container.Args)
out.SetJson("Config", container.Config)
out.SetJson("State", container.State)
out.SetJson("Image", container.Image)
out.Set("Image", container.ImageID)
out.SetJson("NetworkSettings", container.NetworkSettings)
out.Set("ResolvConfPath", container.ResolvConfPath)
out.Set("HostnamePath", container.HostnamePath)
Expand Down
2 changes: 1 addition & 1 deletion daemon/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status {
out := &engine.Env{}
out.SetJson("Id", container.ID)
out.SetList("Names", names[container.ID])
out.SetJson("Image", daemon.Repositories().ImageName(container.Image))
out.SetJson("Image", daemon.Repositories().ImageName(container.ImageID))
if len(container.Args) > 0 {
args := []string{}
for _, arg := range container.Args {
Expand Down
3 changes: 3 additions & 0 deletions graph/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ func validateRepoName(name string) error {
if name == "" {
return fmt.Errorf("Repository name can't be empty")
}
if name == "scratch" {
return fmt.Errorf("'scratch' is a reserved name")
}
return nil
}

Expand Down
6 changes: 3 additions & 3 deletions integration-cli/docker_cli_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ func TestEventsRedirectStdout(t *testing.T) {

func TestEventsImagePull(t *testing.T) {
since := time.Now().Unix()
pullCmd := exec.Command(dockerBinary, "pull", "scratch")
pullCmd := exec.Command(dockerBinary, "pull", "hello-world")
if out, _, err := runCommandWithOutput(pullCmd); err != nil {
t.Fatalf("pulling the scratch image from has failed: %s, %v", out, err)
t.Fatalf("pulling the hello-world image from has failed: %s, %v", out, err)
}

eventsCmd := exec.Command(dockerBinary, "events",
Expand All @@ -243,7 +243,7 @@ func TestEventsImagePull(t *testing.T) {
events := strings.Split(strings.TrimSpace(out), "\n")
event := strings.TrimSpace(events[len(events)-1])

if !strings.HasSuffix(event, "scratch:latest: pull") {
if !strings.HasSuffix(event, "hello-world:latest: pull") {
t.Fatalf("Missing pull event - got:%q", event)
}

Expand Down
2 changes: 1 addition & 1 deletion integration-cli/docker_cli_inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

func TestInspectImage(t *testing.T) {
imageTest := "scratch"
imageTest := "emptyfs"
imageTestID := "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"
imagesCmd := exec.Command(dockerBinary, "inspect", "--format='{{.Id}}'", imageTest)
out, exitCode, err := runCommandWithOutput(imagesCmd)
Expand Down
6 changes: 3 additions & 3 deletions integration-cli/docker_cli_pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (

// pulling an image from the central registry should work
func TestPullImageFromCentralRegistry(t *testing.T) {
pullCmd := exec.Command(dockerBinary, "pull", "scratch")
pullCmd := exec.Command(dockerBinary, "pull", "hello-world")
if out, _, err := runCommandWithOutput(pullCmd); err != nil {
t.Fatalf("pulling the scratch image from the registry has failed: %s, %v", out, err)
t.Fatalf("pulling the hello-world image from the registry has failed: %s, %v", out, err)
}
logDone("pull - pull scratch")
logDone("pull - pull hello-world")
}

// pulling a non-existing image from the central registry should return a non-zero exit code
Expand Down
6 changes: 3 additions & 3 deletions integration-cli/docker_cli_save_load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func TestSaveSingleTag(t *testing.T) {
func TestSaveImageId(t *testing.T) {
repoName := "foobar-save-image-id-test"

tagCmdFinal := fmt.Sprintf("%v tag scratch:latest %v:latest", dockerBinary, repoName)
tagCmdFinal := fmt.Sprintf("%v tag emptyfs:latest %v:latest", dockerBinary, repoName)
tagCmd := exec.Command("bash", "-c", tagCmdFinal)
if out, _, err := runCommandWithOutput(tagCmd); err != nil {
t.Fatalf("failed to tag repo: %s, %v", out, err)
Expand Down Expand Up @@ -370,15 +370,15 @@ func TestSaveMultipleNames(t *testing.T) {
repoName := "foobar-save-multi-name-test"

// Make one image
tagCmdFinal := fmt.Sprintf("%v tag scratch:latest %v-one:latest", dockerBinary, repoName)
tagCmdFinal := fmt.Sprintf("%v tag emptyfs:latest %v-one:latest", dockerBinary, repoName)
tagCmd := exec.Command("bash", "-c", tagCmdFinal)
if out, _, err := runCommandWithOutput(tagCmd); err != nil {
t.Fatalf("failed to tag repo: %s, %v", out, err)
}
defer deleteImages(repoName + "-one")

// Make two images
tagCmdFinal = fmt.Sprintf("%v tag scratch:latest %v-two:latest", dockerBinary, repoName)
tagCmdFinal = fmt.Sprintf("%v tag emptyfs:latest %v-two:latest", dockerBinary, repoName)
tagCmd = exec.Command("bash", "-c", tagCmdFinal)
if out, _, err := runCommandWithOutput(tagCmd); err != nil {
t.Fatalf("failed to tag repo: %s, %v", out, err)
Expand Down
4 changes: 2 additions & 2 deletions project/make/.ensure-scratch
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/bin/bash

if ! docker inspect scratch &> /dev/null; then
# let's build a "docker save" tarball for "scratch"
# let's build a "docker save" tarball for "emptyfs"
# see https://github.com/docker/docker/pull/5262
# and also https://github.com/docker/docker/issues/4242
mkdir -p /docker-scratch
(
cd /docker-scratch
echo '{"scratch":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}' > repositories
echo '{"emptyfs":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}' > repositories
mkdir -p 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
(
cd 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
Expand Down

0 comments on commit 9673beb

Please sign in to comment.