128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ApplyChange applies one `docker import --change` instruction — a
|
|
// Dockerfile line — to config. Supported: ENTRYPOINT, CMD (JSON array
|
|
// or shell form), ENV, WORKDIR, USER, VOLUME (JSON array or space
|
|
// separated), LABEL. Everything else is refused: EXPOSE has no network
|
|
// to speak of, the rest does not describe how to run the image.
|
|
func ApplyChange(config *ImageConfig, change string) error {
|
|
instruction, rest, _ := strings.Cut(strings.TrimSpace(change), " ")
|
|
rest = strings.TrimSpace(rest)
|
|
switch strings.ToUpper(instruction) {
|
|
case "ENTRYPOINT":
|
|
v, err := parseExecForm(rest)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", change, err)
|
|
}
|
|
config.Entrypoint = v
|
|
case "CMD":
|
|
v, err := parseExecForm(rest)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", change, err)
|
|
}
|
|
config.Cmd = v
|
|
case "ENV":
|
|
key, value, err := parseKeyValue(rest)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", change, err)
|
|
}
|
|
config.Env = setEnv(config.Env, key, value)
|
|
case "WORKDIR":
|
|
if rest == "" || !strings.HasPrefix(rest, "/") {
|
|
return fmt.Errorf("%s: WORKDIR needs an absolute path", change)
|
|
}
|
|
config.WorkingDir = rest
|
|
case "USER":
|
|
if rest == "" || strings.ContainsAny(rest, " \t") {
|
|
return fmt.Errorf("%s: USER needs one user[:group], by name or id", change)
|
|
}
|
|
config.User = rest
|
|
case "VOLUME":
|
|
paths, err := parseExecForm(rest)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", change, err)
|
|
}
|
|
if !strings.HasPrefix(rest, "[") {
|
|
paths = strings.Fields(rest)
|
|
}
|
|
for _, p := range paths {
|
|
if !strings.HasPrefix(p, "/") {
|
|
return fmt.Errorf("%s: VOLUME needs absolute paths", change)
|
|
}
|
|
if config.Volumes == nil {
|
|
config.Volumes = map[string]struct{}{}
|
|
}
|
|
config.Volumes[p] = struct{}{}
|
|
}
|
|
case "LABEL":
|
|
key, value, err := parseKeyValue(rest)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", change, err)
|
|
}
|
|
if config.Labels == nil {
|
|
config.Labels = map[string]string{}
|
|
}
|
|
config.Labels[key] = value
|
|
case "":
|
|
return fmt.Errorf("empty --change")
|
|
default:
|
|
return fmt.Errorf("%s: unsupported instruction %s (supported: ENTRYPOINT, CMD, ENV, WORKDIR, USER, VOLUME, LABEL)", change, strings.ToUpper(instruction))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseExecForm reads a JSON array (exec form); anything else is the
|
|
// shell form, which docker wraps in /bin/sh -c.
|
|
func parseExecForm(s string) ([]string, error) {
|
|
if strings.HasPrefix(s, "[") {
|
|
var v []string
|
|
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
|
return nil, fmt.Errorf("exec form must be a JSON array of strings: %w", err)
|
|
}
|
|
return v, nil
|
|
}
|
|
if s == "" {
|
|
return nil, fmt.Errorf("missing value")
|
|
}
|
|
return []string{"/bin/sh", "-c", s}, nil
|
|
}
|
|
|
|
// parseKeyValue accepts KEY=VALUE (optionally double-quoted) and the
|
|
// legacy KEY VALUE form.
|
|
func parseKeyValue(s string) (string, string, error) {
|
|
key, value, found := strings.Cut(s, "=")
|
|
if !found {
|
|
key, value, found = strings.Cut(s, " ")
|
|
if !found {
|
|
return "", "", fmt.Errorf("expected KEY=VALUE")
|
|
}
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
value = strings.TrimSpace(value)
|
|
if key == "" || strings.ContainsAny(key, " \t") {
|
|
return "", "", fmt.Errorf("invalid key %q", key)
|
|
}
|
|
if len(value) >= 2 && strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
|
|
value = value[1 : len(value)-1]
|
|
}
|
|
return key, value, nil
|
|
}
|
|
|
|
// setEnv replaces an existing KEY= entry or appends, keeping order.
|
|
func setEnv(env []string, key, value string) []string {
|
|
entry := key + "=" + value
|
|
for i, e := range env {
|
|
if strings.HasPrefix(e, key+"=") {
|
|
env[i] = entry
|
|
return env
|
|
}
|
|
}
|
|
return append(env, entry)
|
|
}
|