Repository: openllb/hlb
Pull request #149 (2020-05-18 22:26:27)

Add most Dockerfile metadata instructions
- Adds `cmd`, `entrypoint`, `label`, `expose`, `volumes`, `stopSignal`
- Still missing from Dockerfile `HEALTHCHECK`, `MAINTAINER` (deprecated in Dockerfile), `ONBUILD` (will never add), `ADD` (likely never add), `FROM` (we have source ops for this). `HEALTHCHECK` is a little more complicated and we'll need with options for that instruction.
- Only add image spec when exporting as docker image: `dockerLoad`, `dockerPush`, `downloadDockerTarball`. Later we can add the port for `downloadOCITarball` to translate these instructions for OCI.
- In a follow up PR, we need to expose `SetArgs` in upstream `llb`, and allow us to do something like:
```hlb
fs default() {
    image "alpine"
    entrypoint "echo"
    cmd "hello"
    run
}
```


Patch:
diff --git a/builtin/builtin.go b/builtin/builtin.go
index fd60852..dbed4a1 100644
--- a/builtin/builtin.go
+++ b/builtin/builtin.go
@@ -132,6 +132,37 @@ var (
 							parser.NewField(parser.Str, "ref", false),
 						},
 					},
+					"entrypoint": FuncLookup{
+						Params: []*parser.Field{
+							parser.NewField(parser.Str, "args", true),
+						},
+					},
+					"cmd": FuncLookup{
+						Params: []*parser.Field{
+							parser.NewField(parser.Str, "args", true),
+						},
+					},
+					"label": FuncLookup{
+						Params: []*parser.Field{
+							parser.NewField(parser.Str, "key", false),
+							parser.NewField(parser.Str, "value", false),
+						},
+					},
+					"expose": FuncLookup{
+						Params: []*parser.Field{
+							parser.NewField(parser.Str, "ports", true),
+						},
+					},
+					"volumes": FuncLookup{
+						Params: []*parser.Field{
+							parser.NewField(parser.Str, "mountpoints", true),
+						},
+					},
+					"stopSignal": FuncLookup{
+						Params: []*parser.Field{
+							parser.NewField(parser.Str, "signal", false),
+						},
+					},
 				},
 			},
 			"group": LookupByType{
diff --git a/codegen/chain.go b/codegen/chain.go
index 8cadf78..f3639e0 100644
--- a/codegen/chain.go
+++ b/codegen/chain.go
@@ -529,6 +529,107 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 				llb.Copy(input, src, dest, info),
 			), nil
 		}
+	case "entrypoint":
+		var entrypoint []string
+		for _, arg := range args {
+			entrypointArg, err := cg.EmitStringExpr(ctx, scope, arg)
+			if err != nil {
+				return fc, err
+			}
+			entrypoint = append(entrypoint, entrypointArg)
+		}
+
+		cg.image.Config.Entrypoint = entrypoint
+
+		fc = func(st llb.State) (llb.State, error) {
+			// TODO: Expose SetArgs in upstream `llb` package.
+			return st, nil
+		}
+	case "cmd":
+		var cmd []string
+		for _, arg := range args {
+			cmdArg, err := cg.EmitStringExpr(ctx, scope, arg)
+			if err != nil {
+				return fc, err
+			}
+			cmd = append(cmd, cmdArg)
+		}
+
+		cg.image.Config.Cmd = cmd
+
+		fc = func(st llb.State) (llb.State, error) {
+			// TODO: Expose SetArgs in upstream `llb` package.
+			return st, nil
+		}
+	case "label":
+		key, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		value, err := cg.EmitStringExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		if cg.image.Config.Labels == nil {
+			cg.image.Config.Labels = make(map[string]string)
+		}
+		cg.image.Config.Labels[key] = value
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st, nil
+		}
+	case "expose":
+		var ports []string
+		for _, arg := range args {
+			port, err := cg.EmitStringExpr(ctx, scope, arg)
+			if err != nil {
+				return fc, err
+			}
+			ports = append(ports, port)
+		}
+
+		if cg.image.Config.ExposedPorts == nil {
+			cg.image.Config.ExposedPorts = make(map[string]struct{})
+		}
+		for _, port := range ports {
+			cg.image.Config.ExposedPorts[port] = struct{}{}
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st, nil
+		}
+	case "volumes":
+		var mountpoints []string
+		for _, arg := range args {
+			mountpoint, err := cg.EmitStringExpr(ctx, scope, arg)
+			if err != nil {
+				return fc, err
+			}
+			mountpoints = append(mountpoints, mountpoint)
+		}
+
+		if cg.image.Config.Volumes == nil {
+			cg.image.Config.Volumes = make(map[string]struct{})
+		}
+		for _, mountpoint := range mountpoints {
+			cg.image.Config.Volumes[mountpoint] = struct{}{}
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st, nil
+		}
+	case "stopSignal":
+		signal, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		cg.image.Config.StopSignal = signal
+		fc = func(st llb.State) (llb.State, error) {
+			return st, nil
+		}
 	case "dockerPush":
 		ref, err := cg.EmitStringExpr(ctx, scope, args[0])
 		if err != nil {
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 8a27860..cf74114 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -52,9 +52,11 @@ type CodeGen struct {
 	fileSourceByID  map[string]secretsprovider.FileSource
 	agentConfigByID map[string]sockprovider.AgentConfig
 
+	image *specs.Image
+
 	mw        *progress.MultiWriter
 	dockerCli *command.DockerCli
-	solveOpts []solver.SolveOption
+	SolveOpts []solver.SolveOption
 }
 
 type CodeGenOption func(*CodeGen) error
@@ -102,34 +104,6 @@ func (cg *CodeGen) SessionID() string {
 	return cg.sessionID
 }
 
-func (cg *CodeGen) SolveOptions(ctx context.Context, st llb.State) (opts []solver.SolveOption, err error) {
-	env, err := st.Env(ctx)
-	if err != nil {
-		return opts, err
-	}
-
-	args, err := st.GetArgs(ctx)
-	if err != nil {
-		return opts, err
-	}
-
-	dir, err := st.GetDir(ctx)
-	if err != nil {
-		return opts, err
-	}
-
-	opts = append(opts, solver.WithImageSpec(&specs.Image{
-		Config: specs.ImageConfig{
-			Env:        env,
-			Entrypoint: args,
-			WorkingDir: dir,
-		},
-	}))
-
-	opts = append(opts, cg.solveOpts...)
-	return opts, nil
-}
-
 func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []Target) (solver.Request, error) {
 	var requests []solver.Request
 
@@ -227,10 +201,11 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 // If we ever need to parallelize compilation we can revisit this.
 func (cg *CodeGen) reset() {
 	cg.requests = []solver.Request{}
-	cg.solveOpts = []solver.SolveOption{}
+	cg.SolveOpts = []solver.SolveOption{}
 	cg.syncedDirByID = map[string]filesync.SyncedDir{}
 	cg.fileSourceByID = map[string]secretsprovider.FileSource{}
 	cg.agentConfigByID = map[string]sockprovider.AgentConfig{}
+	cg.image = &specs.Image{}
 }
 
 func (cg *CodeGen) newSession(ctx context.Context) (*session.Session, error) {
@@ -1055,7 +1030,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 					netMode = pb.NetMode_UNSET
 				case "host":
 					netMode = pb.NetMode_HOST
-					cg.solveOpts = append(cg.solveOpts, solver.WithEntitlement(entitlements.EntitlementNetworkHost))
+					cg.SolveOpts = append(cg.SolveOpts, solver.WithEntitlement(entitlements.EntitlementNetworkHost))
 				case "node":
 					netMode = pb.NetMode_NONE
 				default:
@@ -1075,7 +1050,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 					securityMode = pb.SecurityMode_SANDBOX
 				case "insecure":
 					securityMode = pb.SecurityMode_INSECURE
-					cg.solveOpts = append(cg.solveOpts, solver.WithEntitlement(entitlements.EntitlementSecurityInsecure))
+					cg.SolveOpts = append(cg.SolveOpts, solver.WithEntitlement(entitlements.EntitlementSecurityInsecure))
 				default:
 					return opts, errors.WithStack(ErrCodeGen{args[0], errors.Errorf("unknown security mode")})
 				}
@@ -1170,7 +1145,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 					g, _ := errgroup.WithContext(ctx)
 
-					cg.solveOpts = append(cg.solveOpts, solver.WithCallback(func() error {
+					cg.SolveOpts = append(cg.SolveOpts, solver.WithCallback(func() error {
 						defer os.RemoveAll(dir)
 
 						err := l.Close()
diff --git a/codegen/output.go b/codegen/output.go
index dc0e824..f126ec5 100644
--- a/codegen/output.go
+++ b/codegen/output.go
@@ -11,6 +11,7 @@ import (
 	"github.com/docker/cli/cli/flags"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/session/filesync"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
 )
@@ -29,7 +30,8 @@ type Output struct {
 type OutputType int
 
 const (
-	OutputDockerPush OutputType = iota
+	OutputNone OutputType = iota
+	OutputDockerPush
 	OutputDockerLoad
 	OutputDownload
 	OutputDownloadTarball
@@ -37,12 +39,34 @@ const (
 	OutputDownloadDockerTarball
 )
 
-func (cg *CodeGen) outputRequest(ctx context.Context, st llb.State, output Output) (solver.Request, error) {
-	opts, err := cg.SolveOptions(ctx, st)
+func (cg *CodeGen) imageSpec(ctx context.Context, st llb.State) (*specs.Image, error) {
+	var err error
+	cg.image.Config.Env, err = st.Env(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	cg.image.Config.WorkingDir, err = st.GetDir(ctx)
 	if err != nil {
 		return nil, err
 	}
 
+	return cg.image, nil
+}
+
+func (cg *CodeGen) outputRequest(ctx context.Context, st llb.State, output Output) (solver.Request, error) {
+	opts := cg.SolveOpts
+
+	// Only add image spec when exporting as a Docker image.
+	switch output.Type {
+	case OutputDockerPush, OutputDockerLoad, OutputDownloadDockerTarball:
+		cfg, err := cg.imageSpec(ctx, st)
+		if err != nil {
+			return nil, err
+		}
+		opts = append(opts, solver.WithImageSpec(cfg))
+	}
+
 	s, err := cg.newSession(ctx)
 	if err != nil {
 		return nil, err
diff --git a/docs/reference.md b/docs/reference.md
index bf96d47..9b3af62 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1,4 +1,18 @@
 ## <span class='hlb-type'>fs</span> functions
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>cmd</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>args</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>args</span>"
+	the default arguments
+
+Sets the default arguments to the entrypoint of the container.
+
+	#!hlb
+	fs default() {
+		cmd "args"
+	}
+
+
+
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>copy</span>(<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>dst</span>)
 
 !!! info "<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>"
@@ -186,6 +200,21 @@ Downloads the filesystem as a tarball to a local path.
 
 
 
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>entrypoint</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>args</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>args</span>"
+	the command to execute.
+
+Defines a list of arguments to use as the command to execute when the
+container starts.
+
+	#!hlb
+	fs default() {
+		entrypoint "args"
+	}
+
+
+
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>env</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
@@ -203,6 +232,22 @@ block.
 
 
 
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>expose</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>ports</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>ports</span>"
+	the set of ports to expose.
+
+Exposes a set of network ports at runtime. The default is TCP if the protocol
+is not specified.
+This metadata is only useful when exporting as a Docker image.
+
+	#!hlb
+	fs default() {
+		expose "ports"
+	}
+
+
+
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>frontend</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>source</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>source</span>"
@@ -325,6 +370,22 @@ Resolves the OCI Image Config and inherit its environment, working directory,
 and entrypoint.
 
 
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>label</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
+	the metadata key.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>"
+	the metadata value.
+
+Sets arbitrary metadata for the container.
+
+	#!hlb
+	fs default() {
+		label "key" "value"
+	}
+
+
+
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>local</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
@@ -631,6 +692,24 @@ methods. By default, this is [&quot;sh&quot;, &quot;-c&quot;].
 
 
 
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>stopSignal</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>signal</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>signal</span>"
+	
+
+Sets the system call signal that will be sent to the container to exit.
+This signal can be a valid unsigned number that matches a position in the
+kernel&#x27;s syscall table, for instance 9, or a signal in the format SIGNAME,
+for instance SIGKILL.
+This metadata is only useful when exporting as a Docker image.
+
+	#!hlb
+	fs default() {
+		stopSignal "signal"
+	}
+
+
+
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>user</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
@@ -645,6 +724,22 @@ Sets the current user for all subsequent calls in this filesystem block.
 
 
 
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>volumes</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>mountpoints</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>mountpoints</span>"
+	the set of mountpoints to mark.
+
+Defines a set of mount points and marks it as holding externally mounted
+volumes from native host or other containers.
+This metadata is only useful when exporting as a Docker image.
+
+	#!hlb
+	fs default() {
+		volumes "mountpoints"
+	}
+
+
+
 ## <span class='hlb-type'>string</span> functions
 ### <span class='hlb-type'>string</span> <span class='hlb-name'>format</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>formatString</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>values</span>)
 
diff --git a/language/builtin.hlb b/language/builtin.hlb
index 035fa7c..c32ea02 100644
--- a/language/builtin.hlb
+++ b/language/builtin.hlb
@@ -510,6 +510,56 @@ fs downloadOCITarball(string localPath)
 # image tarball.
 fs downloadDockerTarball(string localPath, string ref)
 
+# Defines a list of arguments to use as the command to execute when the
+# container starts.
+#
+# @param args the command to execute.
+# @return the filesystem with the entrypoint set.
+fs entrypoint(variadic string args)
+
+# Sets the default arguments to the entrypoint of the container.
+#
+# @param args the default arguments
+# @return the filesystem with default arguments to entrypoint set.
+fs cmd(variadic string args)
+
+# Sets arbitrary metadata for the container.
+#
+# @param key the metadata key.
+# @param value the metadata value.
+# @return a filesystem with a metadata key pair set.
+fs label(string key, string value)
+
+# Exposes a set of network ports at runtime. The default is TCP if the protocol
+# is not specified.
+#
+# This metadata is only useful when exporting as a Docker image.
+#
+# @param ports the set of ports to expose.
+# @return the filesystem with exposed ports set.
+fs expose(variadic string ports)
+
+# Defines a set of mount points and marks it as holding externally mounted
+# volumes from native host or other containers.
+#
+# This metadata is only useful when exporting as a Docker image.
+#
+# @param mountpoints the set of mountpoints to mark.
+# @return the filesystem with volumes set.
+fs volumes(variadic string mountpoints)
+
+# Sets the system call signal that will be sent to the container to exit.
+#
+# This signal can be a valid unsigned number that matches a position in the
+# kernel's syscall table, for instance 9, or a signal in the format SIGNAME,
+# for instance SIGKILL.
+#
+# This metadata is only useful when exporting as a Docker image.
+#
+# @param string the stop signal to send to the container.
+# @return the filesystem with the stop signal set.
+fs stopSignal(string signal)
+
 # A format specifier that is interpolated with values.
 #
 # @param formatString the format specifier.
diff --git a/module/resolve.go b/module/resolve.go
index 0f256d8..20c2de2 100644
--- a/module/resolve.go
+++ b/module/resolve.go
@@ -216,11 +216,6 @@ func (r *remoteResolver) Resolve(ctx context.Context, scope *parser.Scope, decl
 
 	var ref gateway.Reference
 
-	opts, err := cg.SolveOptions(ctx, st)
-	if err != nil {
-		return nil, err
-	}
-
 	g.Go(func() error {
 		return solver.Build(ctx, r.cln, nil, pw, func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
 			res, err := c.Solve(ctx, gateway.SolveRequest{
@@ -239,7 +234,7 @@ func (r *remoteResolver) Resolve(ctx context.Context, scope *parser.Scope, decl
 			<-closed
 
 			return gateway.NewResult(), nil
-		}, opts...)
+		}, cg.SolveOpts...)
 	})
 
 	<-resolved
diff --git a/solver/request.go b/solver/request.go
index 2176d14..4797c59 100644
--- a/solver/request.go
+++ b/solver/request.go
@@ -2,6 +2,7 @@ package solver
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -211,6 +212,13 @@ func (o op) Tree(tree treeprint.Tree) error {
 	if o.info.OutputLocalOCITarball {
 		solve.AddNode("downloadOCITarball")
 	}
+	if o.info.ImageSpec != nil {
+		dt, err := json.Marshal(o.info.ImageSpec)
+		if err != nil {
+			return err
+		}
+		solve.AddMetaNode("imageSpec", string(dt))
+	}
 	if len(o.info.Entitlements) > 0 {
 		ent := solve.AddBranch("entitlements")
 		for _, entitlements := range o.info.Entitlements {


Test patch:
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 6809974..ba24097 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/solver/pb"
 	"github.com/moby/buildkit/util/entitlements"
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/openllb/hlb/checker"
 	"github.com/openllb/hlb/parser"
 	"github.com/openllb/hlb/solver"
@@ -737,6 +738,56 @@ func TestCodeGen(t *testing.T) {
 				llb.Mkfile("shlex", os.FileMode(0644), []byte("$HOME\n")),
 			))
 		},
+	}, {
+		"dockerfile meta",
+		[]string{"default"},
+		`
+		fs default() {
+			image "busybox"
+			env "myenv1" "value1"
+			env "myenv2" "value2"
+			env "myenv1" "value3"
+			dir "myworkdir"
+			entrypoint "my" "entrypoint"
+			cmd "my" "cmd"
+			label "mylabel1" "value1"
+			label "mylabel2" "value2"
+			label "mylabel1" "value3"
+			expose "8080/tcp" "9001/udp"
+			volumes "/var/log" "/var/db"
+			stopSignal "SIGKILL"
+			dockerPush "myimage"
+		}
+		`,
+		func(t *testing.T, cg *CodeGen) solver.Request {
+			return solver.Parallel(
+				Expect(t, llb.Image("busybox")),
+				Expect(t, llb.Image("busybox"),
+					solver.WithPushImage("myimage"),
+					solver.WithImageSpec(&specs.Image{
+						Config: specs.ImageConfig{
+							Env:        []string{"myenv2=value2", "myenv1=value3"},
+							WorkingDir: "/myworkdir",
+							Entrypoint: []string{"my", "entrypoint"},
+							Cmd:        []string{"my", "cmd"},
+							Labels: map[string]string{
+								"mylabel1": "value3",
+								"mylabel2": "value2",
+							},
+							ExposedPorts: map[string]struct{}{
+								"8080/tcp": {},
+								"9001/udp": {},
+							},
+							Volumes: map[string]struct{}{
+								"/var/log": {},
+								"/var/db":  {},
+							},
+							StopSignal: "SIGKILL",
+						},
+					}),
+				),
+			)
+		},
 	}} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {