├── .gitignore ├── Earthfile ├── README.md ├── go.mod ├── go.sum ├── ickfile ├── Ickfile ├── builder │ ├── build.go │ ├── caps.go │ └── subrequests.go ├── cmd │ └── dockerfile-frontend │ │ ├── Dockerfile │ │ ├── hack │ │ ├── check-daily-outdated │ │ ├── detect │ │ └── release │ │ ├── main.go │ │ └── version.go ├── command │ └── command.go ├── dockerfile2llb │ ├── convert.go │ ├── convert_norunnetwork.go │ ├── convert_norunsecurity.go │ ├── convert_runmount.go │ ├── convert_runnetwork.go │ ├── convert_runsecurity.go │ ├── convert_secrets.go │ ├── convert_ssh.go │ ├── convert_test.go │ ├── defaultshell.go │ ├── directives.go │ ├── directives_test.go │ ├── image.go │ ├── platform.go │ └── platform_test.go ├── instructions │ ├── bflag.go │ ├── bflag_test.go │ ├── commands.go │ ├── commands_runmount.go │ ├── commands_runnetwork.go │ ├── commands_runsecurity.go │ ├── commands_secrets.go │ ├── commands_ssh.go │ ├── errors_unix.go │ ├── errors_windows.go │ ├── parse.go │ ├── parse_test.go │ ├── support.go │ └── support_test.go ├── parser │ ├── dumper │ │ └── main.go │ ├── errors.go │ ├── json_test.go │ ├── line_parsers.go │ ├── line_parsers_test.go │ ├── parser.go │ ├── parser_test.go │ ├── split_command.go │ ├── testfile-line │ │ └── Dockerfile │ ├── testfiles-negative │ │ ├── empty_dockerfile │ │ │ └── Dockerfile │ │ ├── env_no_value │ │ │ └── Dockerfile │ │ ├── only_comments │ │ │ └── Dockerfile │ │ └── shykes-nested-json │ │ │ └── Dockerfile │ └── testfiles │ │ ├── ADD-COPY-with-JSON │ │ ├── Dockerfile │ │ └── result │ │ ├── args │ │ ├── Dockerfile │ │ └── result │ │ ├── brimstone-consuldock │ │ ├── Dockerfile │ │ └── result │ │ ├── brimstone-docker-consul │ │ ├── Dockerfile │ │ └── result │ │ ├── continue-at-eof │ │ ├── Dockerfile │ │ └── result │ │ ├── continueIndent │ │ ├── Dockerfile │ │ └── result │ │ ├── cpuguy83-nagios │ │ ├── Dockerfile │ │ └── result │ │ ├── docker │ │ ├── Dockerfile │ │ └── result │ │ ├── env │ │ ├── Dockerfile │ │ └── result │ │ ├── escape-after-comment │ │ ├── Dockerfile │ │ └── result │ │ ├── escape-nonewline │ │ ├── Dockerfile │ │ └── result │ │ ├── escape-with-syntax │ │ ├── Dockerfile │ │ └── result │ │ ├── escape │ │ ├── Dockerfile │ │ └── result │ │ ├── escapes │ │ ├── Dockerfile │ │ └── result │ │ ├── flags │ │ ├── Dockerfile │ │ └── result │ │ ├── health │ │ ├── Dockerfile │ │ └── result │ │ ├── influxdb │ │ ├── Dockerfile │ │ └── result │ │ ├── jeztah-invalid-json-json-inside-string-double │ │ ├── Dockerfile │ │ └── result │ │ ├── jeztah-invalid-json-json-inside-string │ │ ├── Dockerfile │ │ └── result │ │ ├── jeztah-invalid-json-single-quotes │ │ ├── Dockerfile │ │ └── result │ │ ├── jeztah-invalid-json-unterminated-bracket │ │ ├── Dockerfile │ │ └── result │ │ ├── jeztah-invalid-json-unterminated-string │ │ ├── Dockerfile │ │ └── result │ │ ├── json │ │ ├── Dockerfile │ │ └── result │ │ ├── kartar-entrypoint-oddities │ │ ├── Dockerfile │ │ └── result │ │ ├── lk4d4-the-edge-case-generator │ │ ├── Dockerfile │ │ └── result │ │ ├── mail │ │ ├── Dockerfile │ │ └── result │ │ ├── multiple-volumes │ │ ├── Dockerfile │ │ └── result │ │ ├── mumble │ │ ├── Dockerfile │ │ └── result │ │ ├── nginx │ │ ├── Dockerfile │ │ └── result │ │ ├── tf2 │ │ ├── Dockerfile │ │ └── result │ │ ├── trailing-backslash │ │ ├── Dockerfile │ │ └── result │ │ ├── weechat │ │ ├── Dockerfile │ │ └── result │ │ └── znc │ │ ├── Dockerfile │ │ └── result └── readme.md └── writellb └── writellb.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | bin -------------------------------------------------------------------------------- /Earthfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine 2 | WORKDIR /src 3 | 4 | ick-frontend: 5 | COPY . . 6 | RUN go build -o /ickfile-frontend -tags "netgo static_build osusergo" ./ickfile/cmd/dockerfile-frontend 7 | ENTRYPOINT ["/ickfile-frontend"] 8 | SAVE IMAGE agbell/ick:latest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compiling-containers 2 | 3 | # front end 4 | build my front end: 5 | 6 | ``` 7 | earthly -i --push +ick-frontend 8 | ``` 9 | 10 | ``` 11 | docker build -f ./dockerfile/cmd/dockerfile-frontend/Dockerfile . -t agbell/frontend 12 | ``` 13 | 14 | ``` 15 | go build ./dockerfile/cmd/dockerfile-frontend/main.go 16 | ``` 17 | 18 | 19 | ## make LLB 20 | Start buildkit: 21 | ``` 22 | docker run --rm --privileged -d --name buildkit moby/buildkit 23 | export BUILDKIT_HOST=docker-container://buildkit 24 | ``` 25 | 26 | Output LLB: 27 | ``` 28 | go run ./writellb/writellb.go | buildctl debug dump-llb | jq . 29 | ``` 30 | 31 | Build the Image: 32 | ``` 33 | go run ./writellb/writellb.go | buildctl build --local context=. --output type=image,name=docker.io/agbell/test,push=true 34 | ``` 35 | 36 | Run the image 37 | ``` 38 | 39 | docker run -it agbell/test:latest /bin/sh 40 | ``` 41 | 42 | ``` 43 | ➜ compiling-containers git:(main) ✗ go run ./writellb/writellb.go | buildctl build --no-cache --local context=. --output type=image,name=docker.io/agbell/test,push=true 44 | [+] Building 1.9s (7/7) FINISHED 45 | => local://context 0.1s 46 | => => transferring context: 48.26kB 0.1s 47 | => docker-image://docker.io/library/alpine:latest 0.0s 48 | => => resolve docker.io/library/alpine:latest 0.6s 49 | => [auth] library/alpine:pull token for registry-1.docker.io 0.0s 50 | => copy /README.md /README.md 0.0s 51 | => /bin/sh -c echo "programmatically built" > /built.txt 0.1s 52 | => exporting to image 1.7s 53 | => => exporting layers 0.1s 54 | => => exporting manifest sha256:7b613d1abdd023824a709c4c97d59087dbdd6368dbccb6b22ed0305d68f083da 0.0s 55 | => => exporting config sha256:ca6a7e01c880886fd55f0e0e7056d63731c164a85e82e0c4833cc84d07b716da 0.0s 56 | => => pushing layers 1.4s 57 | => => pushing manifest for docker.io/agbell/test:latest 0.3s 58 | => [auth] agbell/test:pull,push token for registry-1.docker.io 0.0s 59 | ➜ compiling-containers git:(main) ✗ docker run -it --pull always agbell/test:latest /bin/sh 60 | latest: Pulling from agbell/test 61 | ba3557a56b15: Already exists 62 | e631696b715d: Already exists 63 | 9868e2d03655: Pull complete 64 | Digest: sha256:7b613d1abdd023824a709c4c97d59087dbdd6368dbccb6b22ed0305d68f083da 65 | Status: Downloaded newer image for agbell/test:latest 66 | / # cat built.txt 67 | programmatically built 68 | / # ls README.md 69 | README.md 70 | ``` 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/agbell/compiling-containers 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/containerd/containerd v1.4.1-0.20201117152358-0edc412565dc 7 | github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe 8 | github.com/docker/distribution v2.7.1+incompatible 9 | github.com/docker/docker v20.10.0-beta1.0.20201110211921-af34b94a78a1+incompatible 10 | github.com/docker/go-connections v0.4.0 11 | github.com/google/go-cmp v0.4.1 12 | github.com/moby/buildkit v0.8.1 13 | github.com/opencontainers/image-spec v1.0.1 14 | github.com/pkg/errors v0.9.1 15 | github.com/sirupsen/logrus v1.7.0 16 | github.com/stretchr/testify v1.5.1 17 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 18 | google.golang.org/grpc v1.29.1 19 | ) 20 | -------------------------------------------------------------------------------- /ickfile/Ickfile: -------------------------------------------------------------------------------- 1 | #syntax=agbell/ick 2 | COME_FROM alpine 3 | e222 USER nagios 4 | <- /src 5 | WRITING root 6 | PLEASE echo "Finally a great syntax for creating docker images!" 7 | DO ["/bin/sh"] 8 | ARE_YOU_OK --interval=5s --timeout=3s --retries=3 CMD ls --quiet -------------------------------------------------------------------------------- /ickfile/builder/build.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "context" 7 | "encoding/csv" 8 | "encoding/json" 9 | "fmt" 10 | "net" 11 | "path" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/agbell/compiling-containers/ickfile/dockerfile2llb" 17 | "github.com/agbell/compiling-containers/ickfile/parser" 18 | "github.com/containerd/containerd/platforms" 19 | controlapi "github.com/moby/buildkit/api/services/control" 20 | "github.com/moby/buildkit/client/llb" 21 | "github.com/moby/buildkit/exporter/containerimage/exptypes" 22 | "github.com/moby/buildkit/frontend/dockerfile/dockerignore" 23 | "github.com/moby/buildkit/frontend/gateway/client" 24 | gwpb "github.com/moby/buildkit/frontend/gateway/pb" 25 | "github.com/moby/buildkit/solver/errdefs" 26 | "github.com/moby/buildkit/solver/pb" 27 | "github.com/moby/buildkit/util/apicaps" 28 | specs "github.com/opencontainers/image-spec/specs-go/v1" 29 | "github.com/pkg/errors" 30 | "golang.org/x/sync/errgroup" 31 | ) 32 | 33 | const ( 34 | DefaultLocalNameContext = "context" 35 | DefaultLocalNameDockerfile = "dockerfile" 36 | keyTarget = "target" 37 | keyFilename = "filename" 38 | keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports 39 | keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry 40 | keyCacheNS = "build-arg:BUILDKIT_CACHE_MOUNT_NS" 41 | defaultDockerfileName = "Dockerfile" 42 | dockerignoreFilename = ".dockerignore" 43 | buildArgPrefix = "build-arg:" 44 | labelPrefix = "label:" 45 | keyNoCache = "no-cache" 46 | keyTargetPlatform = "platform" 47 | keyMultiPlatform = "multi-platform" 48 | keyImageResolveMode = "image-resolve-mode" 49 | keyGlobalAddHosts = "add-hosts" 50 | keyForceNetwork = "force-network-mode" 51 | keyOverrideCopyImage = "override-copy-image" // remove after CopyOp implemented 52 | keyNameContext = "contextkey" 53 | keyNameDockerfile = "dockerfilekey" 54 | keyContextSubDir = "contextsubdir" 55 | keyContextKeepGitDir = "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR" 56 | keySyntax = "build-arg:BUILDKIT_SYNTAX" 57 | keyHostname = "hostname" 58 | ) 59 | 60 | var httpPrefix = regexp.MustCompile(`^https?://`) 61 | var gitURLPathWithFragmentSuffix = regexp.MustCompile(`\.git(?:#.+)?$`) 62 | 63 | func Build(ctx context.Context, c client.Client) (*client.Result, error) { 64 | opts := c.BuildOpts().Opts 65 | caps := c.BuildOpts().LLBCaps 66 | gwcaps := c.BuildOpts().Caps 67 | 68 | allowForward, capsError := validateCaps(opts["frontend.caps"]) 69 | if !allowForward && capsError != nil { 70 | return nil, capsError 71 | } 72 | 73 | marshalOpts := []llb.ConstraintsOpt{llb.WithCaps(caps)} 74 | 75 | localNameContext := DefaultLocalNameContext 76 | if v, ok := opts[keyNameContext]; ok { 77 | localNameContext = v 78 | } 79 | 80 | forceLocalDockerfile := false 81 | localNameDockerfile := DefaultLocalNameDockerfile 82 | if v, ok := opts[keyNameDockerfile]; ok { 83 | forceLocalDockerfile = true 84 | localNameDockerfile = v 85 | } 86 | 87 | defaultBuildPlatform := platforms.DefaultSpec() 88 | if workers := c.BuildOpts().Workers; len(workers) > 0 && len(workers[0].Platforms) > 0 { 89 | defaultBuildPlatform = workers[0].Platforms[0] 90 | } 91 | 92 | buildPlatforms := []specs.Platform{defaultBuildPlatform} 93 | targetPlatforms := []*specs.Platform{nil} 94 | if v := opts[keyTargetPlatform]; v != "" { 95 | var err error 96 | targetPlatforms, err = parsePlatforms(v) 97 | if err != nil { 98 | return nil, err 99 | } 100 | } 101 | 102 | resolveMode, err := parseResolveMode(opts[keyImageResolveMode]) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | extraHosts, err := parseExtraHosts(opts[keyGlobalAddHosts]) 108 | if err != nil { 109 | return nil, errors.Wrap(err, "failed to parse additional hosts") 110 | } 111 | 112 | defaultNetMode, err := parseNetMode(opts[keyForceNetwork]) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | filename := opts[keyFilename] 118 | if filename == "" { 119 | filename = defaultDockerfileName 120 | } 121 | 122 | var ignoreCache []string 123 | if v, ok := opts[keyNoCache]; ok { 124 | if v == "" { 125 | ignoreCache = []string{} // means all stages 126 | } else { 127 | ignoreCache = strings.Split(v, ",") 128 | } 129 | } 130 | 131 | name := "load build definition from " + filename 132 | 133 | filenames := []string{filename, filename + ".dockerignore"} 134 | 135 | // dockerfile is also supported casing moby/moby#10858 136 | if path.Base(filename) == defaultDockerfileName { 137 | filenames = append(filenames, path.Join(path.Dir(filename), strings.ToLower(defaultDockerfileName))) 138 | } 139 | 140 | src := llb.Local(localNameDockerfile, 141 | llb.FollowPaths(filenames), 142 | llb.SessionID(c.BuildOpts().SessionID), 143 | llb.SharedKeyHint(localNameDockerfile), 144 | dockerfile2llb.WithInternalName(name), 145 | ) 146 | 147 | fileop := useFileOp(opts, &caps) 148 | 149 | var buildContext *llb.State 150 | isNotLocalContext := false 151 | if st, ok := detectGitContext(opts[localNameContext], opts[keyContextKeepGitDir]); ok { 152 | if !forceLocalDockerfile { 153 | src = *st 154 | } 155 | buildContext = st 156 | } else if httpPrefix.MatchString(opts[localNameContext]) { 157 | httpContext := llb.HTTP(opts[localNameContext], llb.Filename("context"), dockerfile2llb.WithInternalName("load remote build context")) 158 | def, err := httpContext.Marshal(ctx, marshalOpts...) 159 | if err != nil { 160 | return nil, errors.Wrapf(err, "failed to marshal httpcontext") 161 | } 162 | res, err := c.Solve(ctx, client.SolveRequest{ 163 | Definition: def.ToPB(), 164 | }) 165 | if err != nil { 166 | return nil, errors.Wrapf(err, "failed to resolve httpcontext") 167 | } 168 | 169 | ref, err := res.SingleRef() 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | dt, err := ref.ReadFile(ctx, client.ReadRequest{ 175 | Filename: "context", 176 | Range: &client.FileRange{ 177 | Length: 1024, 178 | }, 179 | }) 180 | if err != nil { 181 | return nil, errors.Wrapf(err, "failed to read downloaded context") 182 | } 183 | if isArchive(dt) { 184 | if fileop { 185 | bc := llb.Scratch().File(llb.Copy(httpContext, "/context", "/", &llb.CopyInfo{ 186 | AttemptUnpack: true, 187 | })) 188 | if !forceLocalDockerfile { 189 | src = bc 190 | } 191 | buildContext = &bc 192 | } else { 193 | copyImage := opts[keyOverrideCopyImage] 194 | if copyImage == "" { 195 | copyImage = dockerfile2llb.DefaultCopyImage 196 | } 197 | unpack := llb.Image(copyImage, dockerfile2llb.WithInternalName("helper image for file operations")). 198 | Run(llb.Shlex("copy --unpack /src/context /out/"), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("extracting build context")) 199 | unpack.AddMount("/src", httpContext, llb.Readonly) 200 | bc := unpack.AddMount("/out", llb.Scratch()) 201 | if !forceLocalDockerfile { 202 | src = bc 203 | } 204 | buildContext = &bc 205 | } 206 | } else { 207 | filename = "context" 208 | if !forceLocalDockerfile { 209 | src = httpContext 210 | } 211 | buildContext = &httpContext 212 | isNotLocalContext = true 213 | } 214 | } else if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil { 215 | inputs, err := c.Inputs(ctx) 216 | if err != nil { 217 | return nil, errors.Wrapf(err, "failed to get frontend inputs") 218 | } 219 | 220 | if !forceLocalDockerfile { 221 | inputDockerfile, ok := inputs[DefaultLocalNameDockerfile] 222 | if ok { 223 | src = inputDockerfile 224 | } 225 | } 226 | 227 | inputCtx, ok := inputs[DefaultLocalNameContext] 228 | if ok { 229 | buildContext = &inputCtx 230 | isNotLocalContext = true 231 | } 232 | } 233 | 234 | if buildContext != nil { 235 | if sub, ok := opts[keyContextSubDir]; ok { 236 | buildContext = scopeToSubDir(buildContext, fileop, sub) 237 | } 238 | } 239 | 240 | def, err := src.Marshal(ctx, marshalOpts...) 241 | if err != nil { 242 | return nil, errors.Wrapf(err, "failed to marshal local source") 243 | } 244 | 245 | var sourceMap *llb.SourceMap 246 | 247 | eg, ctx2 := errgroup.WithContext(ctx) 248 | var dtDockerfile []byte 249 | var dtDockerignore []byte 250 | var dtDockerignoreDefault []byte 251 | eg.Go(func() error { 252 | res, err := c.Solve(ctx2, client.SolveRequest{ 253 | Definition: def.ToPB(), 254 | }) 255 | if err != nil { 256 | return errors.Wrapf(err, "failed to resolve dockerfile") 257 | } 258 | 259 | ref, err := res.SingleRef() 260 | if err != nil { 261 | return err 262 | } 263 | 264 | dtDockerfile, err = ref.ReadFile(ctx2, client.ReadRequest{ 265 | Filename: filename, 266 | }) 267 | if err != nil { 268 | fallback := false 269 | if path.Base(filename) == defaultDockerfileName { 270 | var err1 error 271 | dtDockerfile, err1 = ref.ReadFile(ctx2, client.ReadRequest{ 272 | Filename: path.Join(path.Dir(filename), strings.ToLower(defaultDockerfileName)), 273 | }) 274 | if err1 == nil { 275 | fallback = true 276 | } 277 | } 278 | if !fallback { 279 | return errors.Wrapf(err, "failed to read dockerfile") 280 | } 281 | } 282 | 283 | sourceMap = llb.NewSourceMap(&src, filename, dtDockerfile) 284 | sourceMap.Definition = def 285 | 286 | dt, err := ref.ReadFile(ctx2, client.ReadRequest{ 287 | Filename: filename + ".dockerignore", 288 | }) 289 | if err == nil { 290 | dtDockerignore = dt 291 | } 292 | return nil 293 | }) 294 | var excludes []string 295 | if !isNotLocalContext { 296 | eg.Go(func() error { 297 | dockerignoreState := buildContext 298 | if dockerignoreState == nil { 299 | st := llb.Local(localNameContext, 300 | llb.SessionID(c.BuildOpts().SessionID), 301 | llb.FollowPaths([]string{dockerignoreFilename}), 302 | llb.SharedKeyHint(localNameContext+"-"+dockerignoreFilename), 303 | dockerfile2llb.WithInternalName("load "+dockerignoreFilename), 304 | ) 305 | dockerignoreState = &st 306 | } 307 | def, err := dockerignoreState.Marshal(ctx, marshalOpts...) 308 | if err != nil { 309 | return err 310 | } 311 | res, err := c.Solve(ctx2, client.SolveRequest{ 312 | Definition: def.ToPB(), 313 | }) 314 | if err != nil { 315 | return err 316 | } 317 | ref, err := res.SingleRef() 318 | if err != nil { 319 | return err 320 | } 321 | dtDockerignoreDefault, err = ref.ReadFile(ctx2, client.ReadRequest{ 322 | Filename: dockerignoreFilename, 323 | }) 324 | if err != nil { 325 | return nil 326 | } 327 | return nil 328 | }) 329 | } 330 | 331 | if err := eg.Wait(); err != nil { 332 | return nil, err 333 | } 334 | 335 | if dtDockerignore == nil { 336 | dtDockerignore = dtDockerignoreDefault 337 | } 338 | if dtDockerignore != nil { 339 | excludes, err = dockerignore.ReadAll(bytes.NewBuffer(dtDockerignore)) 340 | if err != nil { 341 | return nil, errors.Wrap(err, "failed to parse dockerignore") 342 | } 343 | } 344 | 345 | if _, ok := opts["cmdline"]; !ok { 346 | if cmdline, ok := opts[keySyntax]; ok { 347 | p := strings.SplitN(strings.TrimSpace(cmdline), " ", 2) 348 | res, err := forwardGateway(ctx, c, p[0], cmdline) 349 | if err != nil && len(errdefs.Sources(err)) == 0 { 350 | return nil, errors.Wrapf(err, "failed with %s = %s", keySyntax, cmdline) 351 | } 352 | return res, err 353 | } else if ref, cmdline, loc, ok := dockerfile2llb.DetectSyntax(bytes.NewBuffer(dtDockerfile)); ok { 354 | res, err := forwardGateway(ctx, c, ref, cmdline) 355 | if err != nil && len(errdefs.Sources(err)) == 0 { 356 | return nil, wrapSource(err, sourceMap, loc) 357 | } 358 | return res, err 359 | } 360 | } 361 | 362 | if capsError != nil { 363 | return nil, capsError 364 | } 365 | 366 | if res, ok, err := checkSubRequest(ctx, opts); ok { 367 | return res, err 368 | } 369 | 370 | exportMap := len(targetPlatforms) > 1 371 | 372 | if v := opts[keyMultiPlatform]; v != "" { 373 | b, err := strconv.ParseBool(v) 374 | if err != nil { 375 | return nil, errors.Errorf("invalid boolean value %s", v) 376 | } 377 | if !b && exportMap { 378 | return nil, errors.Errorf("returning multiple target plaforms is not allowed") 379 | } 380 | exportMap = b 381 | } 382 | 383 | expPlatforms := &exptypes.Platforms{ 384 | Platforms: make([]exptypes.Platform, len(targetPlatforms)), 385 | } 386 | res := client.NewResult() 387 | 388 | eg, ctx = errgroup.WithContext(ctx) 389 | 390 | for i, tp := range targetPlatforms { 391 | func(i int, tp *specs.Platform) { 392 | eg.Go(func() (err error) { 393 | defer func() { 394 | var el *parser.ErrorLocation 395 | if errors.As(err, &el) { 396 | err = wrapSource(err, sourceMap, el.Location) 397 | } 398 | }() 399 | st, img, err := dockerfile2llb.Dockerfile2LLB(ctx, dtDockerfile, dockerfile2llb.ConvertOpt{ 400 | Target: opts[keyTarget], 401 | MetaResolver: c, 402 | BuildArgs: filter(opts, buildArgPrefix), 403 | Labels: filter(opts, labelPrefix), 404 | CacheIDNamespace: opts[keyCacheNS], 405 | SessionID: c.BuildOpts().SessionID, 406 | BuildContext: buildContext, 407 | Excludes: excludes, 408 | IgnoreCache: ignoreCache, 409 | TargetPlatform: tp, 410 | BuildPlatforms: buildPlatforms, 411 | ImageResolveMode: resolveMode, 412 | PrefixPlatform: exportMap, 413 | ExtraHosts: extraHosts, 414 | ForceNetMode: defaultNetMode, 415 | OverrideCopyImage: opts[keyOverrideCopyImage], 416 | LLBCaps: &caps, 417 | SourceMap: sourceMap, 418 | Hostname: opts[keyHostname], 419 | }) 420 | 421 | if err != nil { 422 | return errors.Wrapf(err, "failed to create LLB definition") 423 | } 424 | 425 | def, err := st.Marshal(ctx) 426 | if err != nil { 427 | return errors.Wrapf(err, "failed to marshal LLB definition") 428 | } 429 | 430 | config, err := json.Marshal(img) 431 | if err != nil { 432 | return errors.Wrapf(err, "failed to marshal image config") 433 | } 434 | 435 | var cacheImports []client.CacheOptionsEntry 436 | // new API 437 | if cacheImportsStr := opts[keyCacheImports]; cacheImportsStr != "" { 438 | var cacheImportsUM []controlapi.CacheOptionsEntry 439 | if err := json.Unmarshal([]byte(cacheImportsStr), &cacheImportsUM); err != nil { 440 | return errors.Wrapf(err, "failed to unmarshal %s (%q)", keyCacheImports, cacheImportsStr) 441 | } 442 | for _, um := range cacheImportsUM { 443 | cacheImports = append(cacheImports, client.CacheOptionsEntry{Type: um.Type, Attrs: um.Attrs}) 444 | } 445 | } 446 | // old API 447 | if cacheFromStr := opts[keyCacheFrom]; cacheFromStr != "" { 448 | cacheFrom := strings.Split(cacheFromStr, ",") 449 | for _, s := range cacheFrom { 450 | im := client.CacheOptionsEntry{ 451 | Type: "registry", 452 | Attrs: map[string]string{ 453 | "ref": s, 454 | }, 455 | } 456 | // FIXME(AkihiroSuda): skip append if already exists 457 | cacheImports = append(cacheImports, im) 458 | } 459 | } 460 | 461 | r, err := c.Solve(ctx, client.SolveRequest{ 462 | Definition: def.ToPB(), 463 | CacheImports: cacheImports, 464 | }) 465 | if err != nil { 466 | return err 467 | } 468 | 469 | ref, err := r.SingleRef() 470 | if err != nil { 471 | return err 472 | } 473 | 474 | if !exportMap { 475 | res.AddMeta(exptypes.ExporterImageConfigKey, config) 476 | res.SetRef(ref) 477 | } else { 478 | p := platforms.DefaultSpec() 479 | if tp != nil { 480 | p = *tp 481 | } 482 | 483 | k := platforms.Format(p) 484 | res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config) 485 | res.AddRef(k, ref) 486 | expPlatforms.Platforms[i] = exptypes.Platform{ 487 | ID: k, 488 | Platform: p, 489 | } 490 | } 491 | return nil 492 | }) 493 | }(i, tp) 494 | } 495 | 496 | if err := eg.Wait(); err != nil { 497 | return nil, err 498 | } 499 | 500 | if exportMap { 501 | dt, err := json.Marshal(expPlatforms) 502 | if err != nil { 503 | return nil, err 504 | } 505 | res.AddMeta(exptypes.ExporterPlatformsKey, dt) 506 | } 507 | 508 | return res, nil 509 | } 510 | 511 | func forwardGateway(ctx context.Context, c client.Client, ref string, cmdline string) (*client.Result, error) { 512 | opts := c.BuildOpts().Opts 513 | if opts == nil { 514 | opts = map[string]string{} 515 | } 516 | opts["cmdline"] = cmdline 517 | opts["source"] = ref 518 | 519 | gwcaps := c.BuildOpts().Caps 520 | var frontendInputs map[string]*pb.Definition 521 | if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil { 522 | inputs, err := c.Inputs(ctx) 523 | if err != nil { 524 | return nil, errors.Wrapf(err, "failed to get frontend inputs") 525 | } 526 | 527 | frontendInputs = make(map[string]*pb.Definition) 528 | for name, state := range inputs { 529 | def, err := state.Marshal(ctx) 530 | if err != nil { 531 | return nil, err 532 | } 533 | frontendInputs[name] = def.ToPB() 534 | } 535 | } 536 | 537 | return c.Solve(ctx, client.SolveRequest{ 538 | Frontend: "gateway.v0", 539 | FrontendOpt: opts, 540 | FrontendInputs: frontendInputs, 541 | }) 542 | } 543 | 544 | func filter(opt map[string]string, key string) map[string]string { 545 | m := map[string]string{} 546 | for k, v := range opt { 547 | if strings.HasPrefix(k, key) { 548 | m[strings.TrimPrefix(k, key)] = v 549 | } 550 | } 551 | return m 552 | } 553 | 554 | func detectGitContext(ref, gitContext string) (*llb.State, bool) { 555 | found := false 556 | if httpPrefix.MatchString(ref) && gitURLPathWithFragmentSuffix.MatchString(ref) { 557 | found = true 558 | } 559 | 560 | keepGit := false 561 | if gitContext != "" { 562 | if v, err := strconv.ParseBool(gitContext); err == nil { 563 | keepGit = v 564 | } 565 | } 566 | 567 | for _, prefix := range []string{"git://", "github.com/", "git@"} { 568 | if strings.HasPrefix(ref, prefix) { 569 | found = true 570 | break 571 | } 572 | } 573 | if !found { 574 | return nil, false 575 | } 576 | 577 | parts := strings.SplitN(ref, "#", 2) 578 | branch := "" 579 | if len(parts) > 1 { 580 | branch = parts[1] 581 | } 582 | gitOpts := []llb.GitOption{dockerfile2llb.WithInternalName("load git source " + ref)} 583 | if keepGit { 584 | gitOpts = append(gitOpts, llb.KeepGitDir()) 585 | } 586 | 587 | st := llb.Git(parts[0], branch, gitOpts...) 588 | return &st, true 589 | } 590 | 591 | func isArchive(header []byte) bool { 592 | for _, m := range [][]byte{ 593 | {0x42, 0x5A, 0x68}, // bzip2 594 | {0x1F, 0x8B, 0x08}, // gzip 595 | {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz 596 | } { 597 | if len(header) < len(m) { 598 | continue 599 | } 600 | if bytes.Equal(m, header[:len(m)]) { 601 | return true 602 | } 603 | } 604 | 605 | r := tar.NewReader(bytes.NewBuffer(header)) 606 | _, err := r.Next() 607 | return err == nil 608 | } 609 | 610 | func parsePlatforms(v string) ([]*specs.Platform, error) { 611 | var pp []*specs.Platform 612 | for _, v := range strings.Split(v, ",") { 613 | p, err := platforms.Parse(v) 614 | if err != nil { 615 | return nil, errors.Wrapf(err, "failed to parse target platform %s", v) 616 | } 617 | p = platforms.Normalize(p) 618 | pp = append(pp, &p) 619 | } 620 | return pp, nil 621 | } 622 | 623 | func parseResolveMode(v string) (llb.ResolveMode, error) { 624 | switch v { 625 | case pb.AttrImageResolveModeDefault, "": 626 | return llb.ResolveModeDefault, nil 627 | case pb.AttrImageResolveModeForcePull: 628 | return llb.ResolveModeForcePull, nil 629 | case pb.AttrImageResolveModePreferLocal: 630 | return llb.ResolveModePreferLocal, nil 631 | default: 632 | return 0, errors.Errorf("invalid image-resolve-mode: %s", v) 633 | } 634 | } 635 | 636 | func parseExtraHosts(v string) ([]llb.HostIP, error) { 637 | if v == "" { 638 | return nil, nil 639 | } 640 | out := make([]llb.HostIP, 0) 641 | csvReader := csv.NewReader(strings.NewReader(v)) 642 | fields, err := csvReader.Read() 643 | if err != nil { 644 | return nil, err 645 | } 646 | for _, field := range fields { 647 | parts := strings.SplitN(field, "=", 2) 648 | if len(parts) != 2 { 649 | return nil, errors.Errorf("invalid key-value pair %s", field) 650 | } 651 | key := strings.ToLower(parts[0]) 652 | val := strings.ToLower(parts[1]) 653 | ip := net.ParseIP(val) 654 | if ip == nil { 655 | return nil, errors.Errorf("failed to parse IP %s", val) 656 | } 657 | out = append(out, llb.HostIP{Host: key, IP: ip}) 658 | } 659 | return out, nil 660 | } 661 | 662 | func parseNetMode(v string) (pb.NetMode, error) { 663 | if v == "" { 664 | return llb.NetModeSandbox, nil 665 | } 666 | switch v { 667 | case "none": 668 | return llb.NetModeNone, nil 669 | case "host": 670 | return llb.NetModeHost, nil 671 | case "sandbox": 672 | return llb.NetModeSandbox, nil 673 | default: 674 | return 0, errors.Errorf("invalid netmode %s", v) 675 | } 676 | } 677 | 678 | func useFileOp(args map[string]string, caps *apicaps.CapSet) bool { 679 | enabled := true 680 | if v, ok := args["build-arg:BUILDKIT_DISABLE_FILEOP"]; ok { 681 | if b, err := strconv.ParseBool(v); err == nil { 682 | enabled = !b 683 | } 684 | } 685 | return enabled && caps != nil && caps.Supports(pb.CapFileBase) == nil 686 | } 687 | 688 | func scopeToSubDir(c *llb.State, fileop bool, dir string) *llb.State { 689 | if fileop { 690 | bc := llb.Scratch().File(llb.Copy(*c, dir, "/", &llb.CopyInfo{ 691 | CopyDirContentsOnly: true, 692 | })) 693 | return &bc 694 | } 695 | unpack := llb.Image(dockerfile2llb.DefaultCopyImage, dockerfile2llb.WithInternalName("helper image for file operations")). 696 | Run(llb.Shlexf("copy %s/. /out/", path.Join("/src", dir)), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("filtering build context")) 697 | unpack.AddMount("/src", *c, llb.Readonly) 698 | bc := unpack.AddMount("/out", llb.Scratch()) 699 | return &bc 700 | } 701 | 702 | func wrapSource(err error, sm *llb.SourceMap, ranges []parser.Range) error { 703 | if sm == nil { 704 | return err 705 | } 706 | s := errdefs.Source{ 707 | Info: &pb.SourceInfo{ 708 | Data: sm.Data, 709 | Filename: sm.Filename, 710 | Definition: sm.Definition.ToPB(), 711 | }, 712 | Ranges: make([]*pb.Range, 0, len(ranges)), 713 | } 714 | for _, r := range ranges { 715 | s.Ranges = append(s.Ranges, &pb.Range{ 716 | Start: pb.Position{ 717 | Line: int32(r.Start.Line), 718 | Character: int32(r.Start.Character), 719 | }, 720 | End: pb.Position{ 721 | Line: int32(r.End.Line), 722 | Character: int32(r.End.Character), 723 | }, 724 | }) 725 | } 726 | return errdefs.WithSource(err, s) 727 | } 728 | -------------------------------------------------------------------------------- /ickfile/builder/caps.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/moby/buildkit/solver/errdefs" 7 | "github.com/moby/buildkit/util/grpcerrors" 8 | "github.com/moby/buildkit/util/stack" 9 | "google.golang.org/grpc/codes" 10 | ) 11 | 12 | var enabledCaps = map[string]struct{}{ 13 | "moby.buildkit.frontend.inputs": {}, 14 | "moby.buildkit.frontend.subrequests": {}, 15 | } 16 | 17 | func validateCaps(req string) (forward bool, err error) { 18 | if req == "" { 19 | return 20 | } 21 | caps := strings.Split(req, ",") 22 | for _, c := range caps { 23 | parts := strings.SplitN(c, "+", 2) 24 | if _, ok := enabledCaps[parts[0]]; !ok { 25 | err = stack.Enable(grpcerrors.WrapCode(errdefs.NewUnsupportedFrontendCapError(parts[0]), codes.Unimplemented)) 26 | if strings.Contains(c, "+forward") { 27 | forward = true 28 | } else { 29 | return false, err 30 | } 31 | } 32 | } 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /ickfile/builder/subrequests.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/moby/buildkit/frontend/gateway/client" 8 | "github.com/moby/buildkit/frontend/subrequests" 9 | "github.com/moby/buildkit/solver/errdefs" 10 | ) 11 | 12 | func checkSubRequest(ctx context.Context, opts map[string]string) (*client.Result, bool, error) { 13 | req, ok := opts["requestid"] 14 | if !ok { 15 | return nil, false, nil 16 | } 17 | switch req { 18 | case subrequests.RequestSubrequestsDescribe: 19 | res, err := describe() 20 | return res, true, err 21 | default: 22 | return nil, true, errdefs.NewUnsupportedSubrequestError(req) 23 | } 24 | } 25 | 26 | func describe() (*client.Result, error) { 27 | all := []subrequests.Request{ 28 | subrequests.SubrequestsDescribeDefinition, 29 | } 30 | dt, err := json.MarshalIndent(all, " ", "") 31 | if err != nil { 32 | return nil, err 33 | } 34 | res := client.NewResult() 35 | res.Metadata = map[string][]byte{ 36 | "result.json": dt, 37 | } 38 | return res, nil 39 | } 40 | -------------------------------------------------------------------------------- /ickfile/cmd/dockerfile-frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.1-experimental 2 | 3 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:golang@sha256:6f7d999551dd471b58f70716754290495690efa8421e0a1fcf18eb11d0c0a537 AS xgo 4 | 5 | FROM --platform=$BUILDPLATFORM golang:1.13-buster AS base 6 | COPY --from=xgo / / 7 | WORKDIR /src 8 | ENV GOFLAGS=-mod=vendor 9 | 10 | FROM base AS version 11 | ARG CHANNEL 12 | # TODO: PKG should be inferred from go modules 13 | # RUN --mount=target=. \ 14 | # PKG=github.com/moby/buildkit/frontend/dockerfile/cmd/dockerfile-frontend VERSION=$(./dockerfile/cmd/dockerfile-frontend/hack/detect "$CHANNEL") REVISION=$(git rev-parse HEAD)$(if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi); \ 15 | # echo "-X main.Version=${VERSION} -X main.Revision=${REVISION} -X main.Package=${PKG}" | tee /tmp/.ldflags; \ 16 | # echo -n "${VERSION}" | tee /tmp/.version; 17 | 18 | FROM base AS build 19 | RUN apt-get update && apt-get --no-install-recommends install -y file 20 | ARG BUILDTAGS="" 21 | ARG TARGETPLATFORM 22 | ENV TARGETPLATFORM=$TARGETPLATFORM 23 | RUN --mount=target=. --mount=type=cache,target=/root/.cache \ 24 | --mount=target=/go/pkg/mod,type=cache \ 25 | # --mount=source=/tmp/.ldflags,target=/tmp/.ldflags,from=version \ 26 | CGO_ENABLED=0 go build -o /dockerfile-frontend -ldflags -tags "$BUILDTAGS netgo static_build osusergo" ./dockerfile/cmd/dockerfile-frontend && \ 27 | file /dockerfile-frontend | grep "statically linked" 28 | 29 | FROM scratch AS release 30 | LABEL moby.buildkit.frontend.network.none="true" 31 | LABEL moby.buildkit.frontend.caps="moby.buildkit.frontend.inputs,moby.buildkit.frontend.subrequests" 32 | COPY --from=build /dockerfile-frontend /bin/dockerfile-frontend 33 | ENTRYPOINT ["/bin/dockerfile-frontend"] 34 | 35 | 36 | FROM base AS buildid-check 37 | RUN apt-get update && apt-get --no-install-recommends install -y jq 38 | COPY /dockerfile/cmd/dockerfile-frontend/hack/check-daily-outdated . 39 | COPY --from=r.j3ss.co/reg /usr/bin/reg /bin 40 | COPY --from=build /dockerfile-frontend . 41 | ARG CHANNEL 42 | ARG REPO 43 | ARG DATE 44 | RUN ./check-daily-outdated $CHANNEL $REPO $DATE /out 45 | 46 | FROM scratch AS buildid 47 | COPY --from=buildid-check /out/ / 48 | 49 | FROM release 50 | -------------------------------------------------------------------------------- /ickfile/cmd/dockerfile-frontend/hack/check-daily-outdated: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | usage() { 4 | echo "./check-daily-outdated channel repo date outdir" 5 | exit 1 6 | } 7 | 8 | if [ "$#" != 4 ]; then 9 | usage 10 | fi 11 | 12 | CHANNEL=$1 13 | REPO=$2 14 | DATE=$3 15 | OUTDIR=$4 16 | 17 | mkdir -p $OUTDIR 18 | 19 | if [ ! -d "$OUTDIR" ]; then 20 | echo "invalid output directory $OUTDIR" 21 | exit 1 22 | fi 23 | 24 | set -x 25 | 26 | reg digest "$REPO:$DATE-$CHANNEL" 27 | if [[ $? == 0 ]]; then 28 | exit 0 29 | fi 30 | 31 | lastTag=$(reg tags $REPO | grep "\-$CHANNEL" | sort -r | head -n 1) 32 | 33 | oldBuildID="" 34 | 35 | if [ ! -z "$lastTag" ]; then 36 | layer=$(reg manifest $REPO:$lastTag | jq -r ".layers[0].digest") 37 | tmpdir=$(mktemp -d -t frontend.XXXXXXXXXX) 38 | reg layer "$REPO@$layer" | tar xvz --strip-components=1 -C $tmpdir 39 | oldBuildID=$(go tool buildid $tmpdir/dockerfile-frontend) 40 | rm $tmpdir/dockerfile-frontend 41 | rm -r $tmpdir 42 | fi 43 | 44 | newBuildID=$(go tool buildid dockerfile-frontend) 45 | 46 | if [ "$oldBuildID" != "$newBuildID" ]; then 47 | echo -n $newBuildID > $OUTDIR/buildid 48 | fi 49 | -------------------------------------------------------------------------------- /ickfile/cmd/dockerfile-frontend/hack/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | usage() { 4 | echo "./detect channel" 5 | exit 1 6 | } 7 | 8 | if [ "$#" == 0 ]; then 9 | usage 10 | fi 11 | 12 | channel=$1 13 | suffix="" 14 | 15 | if [ "$channel" == "mainline" ]; then 16 | channel="" 17 | fi 18 | 19 | if [ ! -z "$channel" ]; then 20 | suffix="-$channel" 21 | fi 22 | 23 | name=$(git describe --always --tags --match "dockerfile/[0-9]*$suffix") 24 | 25 | if [[ ! "$name" =~ "dockerfile" ]]; then 26 | name=${name}$suffix 27 | fi 28 | 29 | echo -n $name -------------------------------------------------------------------------------- /ickfile/cmd/dockerfile-frontend/hack/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . $(dirname $0)/../../../../../hack/util 4 | set -e 5 | 6 | : ${PLATFORMS=linux/amd64} 7 | : ${DAILY_TARGETS=} 8 | 9 | usage() { 10 | echo "$0 (master|tag|daily) (tag|channel) [push]" 11 | exit 1 12 | } 13 | 14 | if [ $# != 4 ]; then 15 | usage 16 | fi 17 | 18 | parseTag() { 19 | local prefix=$(echo $1 | cut -d/ -f 1) 20 | if [[ "$prefix" != "dockerfile" ]]; then 21 | echo "invalid tag $1" 22 | exit 1 23 | fi 24 | local suffix=$(echo $1 | awk -F- '{print $NF}') 25 | local tagf=./frontend/dockerfile/release/$suffix/tags 26 | if [ "$sufffix" == "$1" ] || [ ! -f $tagf ]; then 27 | suffix="mainline" 28 | fi 29 | 30 | local mainTag=$(echo $1 | cut -d/ -f 2) 31 | 32 | publishedNames=$REPO:$mainTag 33 | 34 | local versioned="" 35 | # \d.\d.\d becomes latest 36 | if [[ "$mainTag" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 37 | publishedNames=${publishedNames},$REPO:latest 38 | versioned=1 39 | fi 40 | 41 | # \d.\d.\d-channel becomes 42 | if [[ "$mainTag" =~ ^[0-9]+\.[0-9]+\.[0-9]+-$suffix$ ]] && [ -f $tagf ]; then 43 | publishedNames=${publishedNames},$REPO:$suffix 44 | versioned=1 45 | fi 46 | 47 | # \d.\d.\d* -> \d.\d* -> \d* (except "0") 48 | if [ "$versioned" == "1" ]; then 49 | publishedNames=${publishedNames},$REPO:$(echo $mainTag | sed -E 's#^([0-9]+\.[0-9]+)\.[0-9]+#\1#') 50 | if [ "$(echo $mainTag | sed -E 's#^([0-9]+)\.[0-9]+\.[0-9]+.*$#\1#')" != "0" ]; then 51 | publishedNames=${publishedNames},$REPO:$(echo $mainTag | sed -E 's#^([0-9]+)\.[0-9]+\.[0-9]+#\1#') 52 | fi 53 | fi 54 | 55 | TAG=$suffix 56 | } 57 | 58 | TYP=$1 59 | TAG=$2 60 | REPO=$3 61 | PUSH=$4 62 | 63 | pushFlag="push=false" 64 | if [ "$PUSH" = "push" ]; then 65 | pushFlag="push=true" 66 | fi 67 | 68 | importCacheFlags="" 69 | exportCacheFlags="" 70 | if [ "$GITHUB_ACTIONS" = "true" ]; then 71 | if [ -n "$cacheRefFrom" ]; then 72 | importCacheFlags="--cache-from=type=local,src=$cacheRefFrom" 73 | fi 74 | if [ -n "$cacheRefTo" ]; then 75 | exportCacheFlags="--cache-to=type=local,dest=$cacheRefTo" 76 | fi 77 | fi 78 | 79 | case $TYP in 80 | "master") 81 | tagf=./frontend/dockerfile/release/$TAG/tags 82 | if [ ! -f $tagf ]; then 83 | echo "invalid release $TAG" 84 | exit 1 85 | fi 86 | 87 | buildTags=$(cat $tagf) 88 | pushTag="master" 89 | if [ "$TAG" != "mainline" ]; then 90 | pushTag=${pushTag}-$TAG 91 | fi 92 | 93 | buildxCmd build $importCacheFlags $exportCacheFlags \ 94 | --platform "$PLATFORMS" \ 95 | --build-arg "CHANNEL=$TAG" \ 96 | --build-arg "BUILDTAGS=$buildTags" \ 97 | --output "type=image,name=$REPO:$pushTag,$pushFlag" \ 98 | --file "./frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile" \ 99 | $currentcontext 100 | ;; 101 | "tag") 102 | publishedNames="" 103 | parseTag $TAG 104 | tagf=./frontend/dockerfile/release/$TAG/tags 105 | if [ ! -f $tagf ]; then 106 | echo "no build tags found for $TAG" 107 | exit 1 108 | fi 109 | buildTags=$(cat $tagf) 110 | 111 | buildxCmd build $importCacheFlags $exportCacheFlags \ 112 | --platform "$PLATFORMS" \ 113 | --build-arg "CHANNEL=$TAG" \ 114 | --build-arg "BUILDTAGS=$buildTags" \ 115 | --output "type=image,\"name=$publishedNames\",$pushFlag" \ 116 | --file "./frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile" \ 117 | $currentcontext 118 | ;; 119 | "daily") 120 | # if [ -z $DAILY_TARGETS ]; then 121 | # DAILY_TARGETS="" 122 | # fi 123 | 124 | for TAG in $DAILY_TARGETS; do 125 | 126 | tagf=./frontend/dockerfile/release/$TAG/tags 127 | if [ ! -f $tagf ]; then 128 | echo "invalid release $TAG" 129 | exit 1 130 | fi 131 | buildTags=$(cat $tagf) 132 | 133 | # find the buildID of the last pushed image 134 | # returns a BuildID if rebuild needed 135 | 136 | tmp=$(mktemp -d -t buildid.XXXXXXXXXX) 137 | dt=$(date +%Y%m%d) 138 | buildxCmd build $importCacheFlags $exportCacheFlags \ 139 | --platform "$PLATFORMS" \ 140 | --target "buildid" \ 141 | --build-arg "CHANNEL=$TAG" \ 142 | --build-arg "BUILDTAGS=$buildTags" \ 143 | --build-arg "REPO=$REPO" \ 144 | --build-arg "DATE=$dt" \ 145 | --output "type=local,dest=$tmp" \ 146 | --file "./frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile" \ 147 | $currentcontext 148 | 149 | if [ -f $tmp/buildid ]; then 150 | buildid=$(cat $tmp/buildid) 151 | echo "buildid: $buildid" 152 | 153 | buildxCmd build $importCacheFlags $exportCacheFlags \ 154 | --platform "$PLATFORMS" \ 155 | --build-arg "CHANNEL=$TAG" \ 156 | --build-arg "BUILDTAGS=$buildTags" \ 157 | --output "type=image,name=$REPO:$dt-$TAG,$pushFlag" \ 158 | --file "./frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile" \ 159 | $currentcontext 160 | rm $tmp/buildid 161 | fi 162 | rm -r $tmp 163 | 164 | done 165 | 166 | ;; 167 | esac 168 | -------------------------------------------------------------------------------- /ickfile/cmd/dockerfile-frontend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | dockerfile "github.com/agbell/compiling-containers/ickfile/builder" 5 | "github.com/moby/buildkit/frontend/gateway/grpcclient" 6 | "github.com/moby/buildkit/util/appcontext" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func main() { 11 | if err := grpcclient.RunFromEnvironment(appcontext.Context(), dockerfile.Build); err != nil { 12 | logrus.Errorf("fatal error: %+v", err) 13 | panic(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ickfile/cmd/dockerfile-frontend/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | // Package is filled at linking time 5 | Package = "github.com/moby/buildkit/frontend/dockerfile/cmd/dockerfile-frontend" 6 | 7 | // Version holds the complete version number. Filled in at linking time. 8 | Version = "0.0.0+unknown" 9 | 10 | // Revision is filled with the VCS (e.g. git) revision being used to build 11 | // the program at linking time. 12 | Revision = "" 13 | ) 14 | -------------------------------------------------------------------------------- /ickfile/command/command.go: -------------------------------------------------------------------------------- 1 | // Package command contains the set of Dockerfile commands. 2 | package command 3 | 4 | // Define constants for the command strings 5 | const ( 6 | Add = "add" 7 | Arg = "var" 8 | Cmd = "do" 9 | Copy = "stash" 10 | Entrypoint = "please_do" 11 | Env = "e222" 12 | Expose = "expose" 13 | From = "come_from" 14 | Healthcheck = "are_you_ok" 15 | Label = "e252" 16 | Maintainer = "e256" 17 | Onbuild = "abstain" 18 | Run = "please" 19 | Shell = "mystery" 20 | StopSignal = "i_do_not_compute" 21 | User = "writing" 22 | Volume = "->" 23 | Workdir = "<-" 24 | ) 25 | 26 | // Commands is list of all Dockerfile commands 27 | var Commands = map[string]struct{}{ 28 | Add: {}, 29 | Arg: {}, 30 | Cmd: {}, 31 | Copy: {}, 32 | Entrypoint: {}, 33 | Env: {}, 34 | Expose: {}, 35 | From: {}, 36 | Healthcheck: {}, 37 | Label: {}, 38 | Maintainer: {}, 39 | Onbuild: {}, 40 | Run: {}, 41 | Shell: {}, 42 | StopSignal: {}, 43 | User: {}, 44 | Volume: {}, 45 | Workdir: {}, 46 | } 47 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_norunnetwork.go: -------------------------------------------------------------------------------- 1 | // +build !dfrunnetwork 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/agbell/compiling-containers/ickfile/instructions" 7 | "github.com/moby/buildkit/client/llb" 8 | ) 9 | 10 | func dispatchRunNetwork(c *instructions.RunCommand) (llb.RunOption, error) { 11 | return nil, nil 12 | } 13 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_norunsecurity.go: -------------------------------------------------------------------------------- 1 | // +build !dfrunsecurity 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/agbell/compiling-containers/ickfile/instructions" 7 | "github.com/moby/buildkit/client/llb" 8 | ) 9 | 10 | func dispatchRunSecurity(c *instructions.RunCommand) (llb.RunOption, error) { 11 | return nil, nil 12 | } 13 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_runmount.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/agbell/compiling-containers/ickfile/instructions" 13 | "github.com/moby/buildkit/client/llb" 14 | "github.com/moby/buildkit/solver/pb" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | func detectRunMount(cmd *command, allDispatchStates *dispatchStates) bool { 19 | if c, ok := cmd.Command.(*instructions.RunCommand); ok { 20 | mounts := instructions.GetMounts(c) 21 | sources := make([]*dispatchState, len(mounts)) 22 | for i, mount := range mounts { 23 | if mount.From == "" && mount.Type == instructions.MountTypeCache { 24 | mount.From = emptyImageName 25 | } 26 | from := mount.From 27 | if from == "" || mount.Type == instructions.MountTypeTmpfs { 28 | continue 29 | } 30 | stn, ok := allDispatchStates.findStateByName(from) 31 | if !ok { 32 | stn = &dispatchState{ 33 | stage: instructions.Stage{BaseName: from}, 34 | deps: make(map[*dispatchState]struct{}), 35 | unregistered: true, 36 | } 37 | } 38 | sources[i] = stn 39 | } 40 | cmd.sources = sources 41 | return true 42 | } 43 | 44 | return false 45 | } 46 | 47 | func setCacheUIDGIDFileOp(m *instructions.Mount, st llb.State) llb.State { 48 | uid := 0 49 | gid := 0 50 | mode := os.FileMode(0755) 51 | if m.UID != nil { 52 | uid = int(*m.UID) 53 | } 54 | if m.GID != nil { 55 | gid = int(*m.GID) 56 | } 57 | if m.Mode != nil { 58 | mode = os.FileMode(*m.Mode) 59 | } 60 | return st.File(llb.Mkdir("/cache", mode, llb.WithUIDGID(uid, gid)), llb.WithCustomName("[internal] settings cache mount permissions")) 61 | } 62 | 63 | func setCacheUIDGID(m *instructions.Mount, st llb.State, fileop bool) llb.State { 64 | if fileop { 65 | return setCacheUIDGIDFileOp(m, st) 66 | } 67 | 68 | var b strings.Builder 69 | if m.UID != nil { 70 | b.WriteString(fmt.Sprintf("chown %d /mnt/cache;", *m.UID)) 71 | } 72 | if m.GID != nil { 73 | b.WriteString(fmt.Sprintf("chown :%d /mnt/cache;", *m.GID)) 74 | } 75 | if m.Mode != nil { 76 | b.WriteString(fmt.Sprintf("chmod %s /mnt/cache;", strconv.FormatUint(*m.Mode, 8))) 77 | } 78 | return llb.Image("busybox").Run(llb.Shlex(fmt.Sprintf("sh -c 'mkdir -p /mnt/cache;%s'", b.String())), llb.WithCustomName("[internal] settings cache mount permissions")).AddMount("/mnt", st) 79 | } 80 | 81 | func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*dispatchState, opt dispatchOpt) ([]llb.RunOption, error) { 82 | var out []llb.RunOption 83 | mounts := instructions.GetMounts(c) 84 | 85 | for i, mount := range mounts { 86 | if mount.From == "" && mount.Type == instructions.MountTypeCache { 87 | mount.From = emptyImageName 88 | } 89 | st := opt.buildContext 90 | if mount.From != "" { 91 | st = sources[i].state 92 | } 93 | var mountOpts []llb.MountOption 94 | if mount.Type == instructions.MountTypeTmpfs { 95 | st = llb.Scratch() 96 | mountOpts = append(mountOpts, llb.Tmpfs()) 97 | } 98 | if mount.Type == instructions.MountTypeSecret { 99 | secret, err := dispatchSecret(mount) 100 | if err != nil { 101 | return nil, err 102 | } 103 | out = append(out, secret) 104 | continue 105 | } 106 | if mount.Type == instructions.MountTypeSSH { 107 | ssh, err := dispatchSSH(mount) 108 | if err != nil { 109 | return nil, err 110 | } 111 | out = append(out, ssh) 112 | continue 113 | } 114 | if mount.ReadOnly { 115 | mountOpts = append(mountOpts, llb.Readonly) 116 | } else if mount.Type == instructions.MountTypeBind && opt.llbCaps.Supports(pb.CapExecMountBindReadWriteNoOuput) == nil { 117 | mountOpts = append(mountOpts, llb.ForceNoOutput) 118 | } 119 | if mount.Type == instructions.MountTypeCache { 120 | sharing := llb.CacheMountShared 121 | if mount.CacheSharing == instructions.MountSharingPrivate { 122 | sharing = llb.CacheMountPrivate 123 | } 124 | if mount.CacheSharing == instructions.MountSharingLocked { 125 | sharing = llb.CacheMountLocked 126 | } 127 | if mount.CacheID == "" { 128 | mount.CacheID = path.Clean(mount.Target) 129 | } 130 | mountOpts = append(mountOpts, llb.AsPersistentCacheDir(opt.cacheIDNamespace+"/"+mount.CacheID, sharing)) 131 | } 132 | target := mount.Target 133 | if !filepath.IsAbs(filepath.Clean(mount.Target)) { 134 | dir, err := d.state.GetDir(context.TODO()) 135 | if err != nil { 136 | return nil, err 137 | } 138 | target = filepath.Join("/", dir, mount.Target) 139 | } 140 | if target == "/" { 141 | return nil, errors.Errorf("invalid mount target %q", target) 142 | } 143 | if src := path.Join("/", mount.Source); src != "/" { 144 | mountOpts = append(mountOpts, llb.SourcePath(src)) 145 | } else { 146 | if mount.UID != nil || mount.GID != nil || mount.Mode != nil { 147 | st = setCacheUIDGID(mount, st, useFileOp(opt.buildArgValues, opt.llbCaps)) 148 | mountOpts = append(mountOpts, llb.SourcePath("/cache")) 149 | } 150 | } 151 | 152 | out = append(out, llb.AddMount(target, st, mountOpts...)) 153 | 154 | if mount.From == "" { 155 | d.ctxPaths[path.Join("/", filepath.ToSlash(mount.Source))] = struct{}{} 156 | } 157 | } 158 | return out, nil 159 | } 160 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_runnetwork.go: -------------------------------------------------------------------------------- 1 | // +build dfrunnetwork 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/pkg/errors" 7 | 8 | "github.com/agbell/compiling-containers/ickfile/instructions" 9 | "github.com/moby/buildkit/client/llb" 10 | "github.com/moby/buildkit/solver/pb" 11 | ) 12 | 13 | func dispatchRunNetwork(c *instructions.RunCommand) (llb.RunOption, error) { 14 | network := instructions.GetNetwork(c) 15 | 16 | switch network { 17 | case instructions.NetworkDefault: 18 | return nil, nil 19 | case instructions.NetworkNone: 20 | return llb.Network(pb.NetMode_NONE), nil 21 | case instructions.NetworkHost: 22 | return llb.Network(pb.NetMode_HOST), nil 23 | default: 24 | return nil, errors.Errorf("unsupported network mode %q", network) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_runsecurity.go: -------------------------------------------------------------------------------- 1 | // +build dfrunsecurity 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/pkg/errors" 7 | 8 | "github.com/agbell/compiling-containers/ickfile/instructions" 9 | "github.com/moby/buildkit/client/llb" 10 | "github.com/moby/buildkit/solver/pb" 11 | ) 12 | 13 | func dispatchRunSecurity(c *instructions.RunCommand) (llb.RunOption, error) { 14 | security := instructions.GetSecurity(c) 15 | 16 | switch security { 17 | case instructions.SecurityInsecure: 18 | return llb.Security(pb.SecurityMode_INSECURE), nil 19 | case instructions.SecuritySandbox: 20 | return llb.Security(pb.SecurityMode_SANDBOX), nil 21 | default: 22 | return nil, errors.Errorf("unsupported security mode %q", security) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_secrets.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/agbell/compiling-containers/ickfile/instructions" 7 | "github.com/moby/buildkit/client/llb" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func dispatchSecret(m *instructions.Mount) (llb.RunOption, error) { 12 | id := m.CacheID 13 | if m.Source != "" { 14 | id = m.Source 15 | } 16 | 17 | if id == "" { 18 | if m.Target == "" { 19 | return nil, errors.Errorf("one of source, target required") 20 | } 21 | id = path.Base(m.Target) 22 | } 23 | 24 | target := m.Target 25 | if target == "" { 26 | target = "/run/secrets/" + path.Base(id) 27 | } 28 | 29 | opts := []llb.SecretOption{llb.SecretID(id)} 30 | 31 | if !m.Required { 32 | opts = append(opts, llb.SecretOptional) 33 | } 34 | 35 | if m.UID != nil || m.GID != nil || m.Mode != nil { 36 | var uid, gid, mode int 37 | if m.UID != nil { 38 | uid = int(*m.UID) 39 | } 40 | if m.GID != nil { 41 | gid = int(*m.GID) 42 | } 43 | if m.Mode != nil { 44 | mode = int(*m.Mode) 45 | } else { 46 | mode = 0400 47 | } 48 | opts = append(opts, llb.SecretFileOpt(uid, gid, mode)) 49 | } 50 | 51 | return llb.AddSecret(target, opts...), nil 52 | } 53 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_ssh.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "github.com/agbell/compiling-containers/ickfile/instructions" 5 | "github.com/moby/buildkit/client/llb" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func dispatchSSH(m *instructions.Mount) (llb.RunOption, error) { 10 | if m.Source != "" { 11 | return nil, errors.Errorf("ssh does not support source") 12 | } 13 | opts := []llb.SSHOption{llb.SSHID(m.CacheID)} 14 | 15 | if m.Target != "" { 16 | opts = append(opts, llb.SSHSocketTarget(m.Target)) 17 | } 18 | 19 | if !m.Required { 20 | opts = append(opts, llb.SSHOptional) 21 | } 22 | 23 | if m.UID != nil || m.GID != nil || m.Mode != nil { 24 | var uid, gid, mode int 25 | if m.UID != nil { 26 | uid = int(*m.UID) 27 | } 28 | if m.GID != nil { 29 | gid = int(*m.GID) 30 | } 31 | if m.Mode != nil { 32 | mode = int(*m.Mode) 33 | } else { 34 | mode = 0600 35 | } 36 | opts = append(opts, llb.SSHSocketOpt(m.Target, uid, gid, mode)) 37 | } 38 | 39 | return llb.AddSSHSocket(opts...), nil 40 | } 41 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/convert_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/agbell/compiling-containers/ickfile/instructions" 7 | "github.com/moby/buildkit/frontend/dockerfile/shell" 8 | "github.com/moby/buildkit/util/appcontext" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func toEnvMap(args []instructions.KeyValuePairOptional, env []string) map[string]string { 13 | m := shell.BuildEnvs(env) 14 | 15 | for _, arg := range args { 16 | // If key already exists, keep previous value. 17 | if _, ok := m[arg.Key]; ok { 18 | continue 19 | } 20 | if arg.Value != nil { 21 | m[arg.Key] = arg.ValueString() 22 | } 23 | } 24 | return m 25 | } 26 | 27 | func TestDockerfileParsing(t *testing.T) { 28 | t.Parallel() 29 | df := `FROM scratch 30 | ENV FOO bar 31 | COPY f1 f2 /sub/ 32 | PLEASE ls -l 33 | ` 34 | _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 35 | assert.NoError(t, err) 36 | 37 | df = `FROM scratch AS foo 38 | ENV FOO bar 39 | FROM foo 40 | COPY --from=foo f1 / 41 | COPY --from=0 f2 / 42 | ` 43 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 44 | assert.NoError(t, err) 45 | 46 | df = `FROM scratch AS foo 47 | ENV FOO bar 48 | FROM foo 49 | COPY --from=foo f1 / 50 | COPY --from=0 f2 / 51 | ` 52 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ 53 | Target: "Foo", 54 | }) 55 | assert.NoError(t, err) 56 | 57 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ 58 | Target: "nosuch", 59 | }) 60 | assert.Error(t, err) 61 | 62 | df = `FROM scratch 63 | ADD http://github.com/moby/buildkit/blob/master/README.md / 64 | ` 65 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 66 | assert.NoError(t, err) 67 | 68 | df = `FROM scratch 69 | COPY http://github.com/moby/buildkit/blob/master/README.md / 70 | ` 71 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 72 | assert.EqualError(t, err, "source can't be a URL for COPY") 73 | 74 | df = `FROM "" AS foo` 75 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 76 | assert.Error(t, err) 77 | 78 | df = `FROM ${BLANK} AS foo` 79 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 80 | assert.Error(t, err) 81 | } 82 | 83 | func TestAddEnv(t *testing.T) { 84 | // k exists in env as key 85 | // override = true 86 | env := []string{"key1=val1", "key2=val2"} 87 | result := addEnv(env, "key1", "value1") 88 | assert.Equal(t, []string{"key1=value1", "key2=val2"}, result) 89 | 90 | // k does not exist in env as key 91 | // override = true 92 | env = []string{"key1=val1", "key2=val2"} 93 | result = addEnv(env, "key3", "val3") 94 | assert.Equal(t, []string{"key1=val1", "key2=val2", "key3=val3"}, result) 95 | 96 | // env has same keys 97 | // override = true 98 | env = []string{"key1=val1", "key1=val2"} 99 | result = addEnv(env, "key1", "value1") 100 | assert.Equal(t, []string{"key1=value1", "key1=val2"}, result) 101 | 102 | // k matches with key only string in env 103 | // override = true 104 | env = []string{"key1=val1", "key2=val2", "key3"} 105 | result = addEnv(env, "key3", "val3") 106 | assert.Equal(t, []string{"key1=val1", "key2=val2", "key3=val3"}, result) 107 | } 108 | 109 | func TestParseKeyValue(t *testing.T) { 110 | k, v := parseKeyValue("key=val") 111 | assert.Equal(t, "key", k) 112 | assert.Equal(t, "val", v) 113 | 114 | k, v = parseKeyValue("key=") 115 | assert.Equal(t, "key", k) 116 | assert.Equal(t, "", v) 117 | 118 | k, v = parseKeyValue("key") 119 | assert.Equal(t, "key", k) 120 | assert.Equal(t, "", v) 121 | } 122 | 123 | func TestToEnvList(t *testing.T) { 124 | // args has no duplicated key with env 125 | v := "val2" 126 | args := []instructions.KeyValuePairOptional{{Key: "key2", Value: &v}} 127 | env := []string{"key1=val1"} 128 | resutl := toEnvMap(args, env) 129 | assert.Equal(t, map[string]string{"key1": "val1", "key2": "val2"}, resutl) 130 | 131 | // value of args is nil 132 | args = []instructions.KeyValuePairOptional{{Key: "key2", Value: nil}} 133 | env = []string{"key1=val1"} 134 | resutl = toEnvMap(args, env) 135 | assert.Equal(t, map[string]string{"key1": "val1"}, resutl) 136 | 137 | // args has duplicated key with env 138 | v = "val2" 139 | args = []instructions.KeyValuePairOptional{{Key: "key1", Value: &v}} 140 | env = []string{"key1=val1"} 141 | resutl = toEnvMap(args, env) 142 | assert.Equal(t, map[string]string{"key1": "val1"}, resutl) 143 | 144 | v = "val2" 145 | args = []instructions.KeyValuePairOptional{{Key: "key1", Value: &v}} 146 | env = []string{"key1="} 147 | resutl = toEnvMap(args, env) 148 | assert.Equal(t, map[string]string{"key1": ""}, resutl) 149 | 150 | v = "val2" 151 | args = []instructions.KeyValuePairOptional{{Key: "key1", Value: &v}} 152 | env = []string{"key1"} 153 | resutl = toEnvMap(args, env) 154 | assert.Equal(t, map[string]string{"key1": ""}, resutl) 155 | 156 | // env has duplicated keys 157 | v = "val2" 158 | args = []instructions.KeyValuePairOptional{{Key: "key2", Value: &v}} 159 | env = []string{"key1=val1", "key1=val1_2"} 160 | resutl = toEnvMap(args, env) 161 | assert.Equal(t, map[string]string{"key1": "val1_2", "key2": "val2"}, resutl) 162 | 163 | // args has duplicated keys 164 | v1 := "v1" 165 | v2 := "v2" 166 | args = []instructions.KeyValuePairOptional{{Key: "key2", Value: &v1}, {Key: "key2", Value: &v2}} 167 | env = []string{"key1=val1"} 168 | resutl = toEnvMap(args, env) 169 | assert.Equal(t, map[string]string{"key1": "val1", "key2": "v1"}, resutl) 170 | } 171 | 172 | func TestDockerfileCircularDependencies(t *testing.T) { 173 | // single stage depends on itself 174 | df := `FROM busybox AS stage0 175 | COPY --from=stage0 f1 /sub/ 176 | ` 177 | _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 178 | assert.EqualError(t, err, "circular dependency detected on stage: stage0") 179 | 180 | // multiple stages with circular dependency 181 | df = `FROM busybox AS stage0 182 | COPY --from=stage2 f1 /sub/ 183 | FROM busybox AS stage1 184 | COPY --from=stage0 f2 /sub/ 185 | FROM busybox AS stage2 186 | COPY --from=stage1 f2 /sub/ 187 | ` 188 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 189 | assert.EqualError(t, err, "circular dependency detected on stage: stage0") 190 | } 191 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/defaultshell.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | func defaultShell(os string) []string { 4 | if os == "windows" { 5 | return []string{"cmd", "/S", "/C"} 6 | } 7 | return []string{"/bin/sh", "-c"} 8 | } 9 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/directives.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/agbell/compiling-containers/ickfile/parser" 10 | ) 11 | 12 | const keySyntax = "syntax" 13 | 14 | var reDirective = regexp.MustCompile(`^#\s*([a-zA-Z][a-zA-Z0-9]*)\s*=\s*(.+?)\s*$`) 15 | 16 | type Directive struct { 17 | Name string 18 | Value string 19 | Location []parser.Range 20 | } 21 | 22 | func DetectSyntax(r io.Reader) (string, string, []parser.Range, bool) { 23 | directives := ParseDirectives(r) 24 | if len(directives) == 0 { 25 | return "", "", nil, false 26 | } 27 | v, ok := directives[keySyntax] 28 | if !ok { 29 | return "", "", nil, false 30 | } 31 | p := strings.SplitN(v.Value, " ", 2) 32 | return p[0], v.Value, v.Location, true 33 | } 34 | 35 | func ParseDirectives(r io.Reader) map[string]Directive { 36 | m := map[string]Directive{} 37 | s := bufio.NewScanner(r) 38 | var l int 39 | for s.Scan() { 40 | l++ 41 | match := reDirective.FindStringSubmatch(s.Text()) 42 | if len(match) == 0 { 43 | return m 44 | } 45 | m[strings.ToLower(match[1])] = Directive{ 46 | Name: match[1], 47 | Value: match[2], 48 | Location: []parser.Range{{ 49 | Start: parser.Position{Line: l}, 50 | End: parser.Position{Line: l}, 51 | }}, 52 | } 53 | } 54 | return m 55 | } 56 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/directives_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDirectives(t *testing.T) { 12 | t.Parallel() 13 | 14 | dt := `#escape=\ 15 | # key = FOO bar 16 | 17 | # smth 18 | ` 19 | 20 | d := ParseDirectives(bytes.NewBuffer([]byte(dt))) 21 | require.Equal(t, len(d), 2, fmt.Sprintf("%+v", d)) 22 | 23 | v, ok := d["escape"] 24 | require.True(t, ok) 25 | require.Equal(t, v.Value, "\\") 26 | 27 | v, ok = d["key"] 28 | require.True(t, ok) 29 | require.Equal(t, v.Value, "FOO bar") 30 | 31 | // for some reason Moby implementation in case insensitive for escape 32 | dt = `# EScape=\ 33 | # KEY = FOO bar 34 | 35 | # smth 36 | ` 37 | 38 | d = ParseDirectives(bytes.NewBuffer([]byte(dt))) 39 | require.Equal(t, len(d), 2, fmt.Sprintf("%+v", d)) 40 | 41 | v, ok = d["escape"] 42 | require.True(t, ok) 43 | require.Equal(t, v.Value, "\\") 44 | 45 | v, ok = d["key"] 46 | require.True(t, ok) 47 | require.Equal(t, v.Value, "FOO bar") 48 | } 49 | 50 | func TestSyntaxDirective(t *testing.T) { 51 | t.Parallel() 52 | 53 | dt := `# syntax = dockerfile:experimental // opts 54 | FROM busybox 55 | ` 56 | 57 | ref, cmdline, loc, ok := DetectSyntax(bytes.NewBuffer([]byte(dt))) 58 | require.True(t, ok) 59 | require.Equal(t, ref, "dockerfile:experimental") 60 | require.Equal(t, cmdline, "dockerfile:experimental // opts") 61 | require.Equal(t, 1, loc[0].Start.Line) 62 | require.Equal(t, 1, loc[0].End.Line) 63 | 64 | dt = `FROM busybox 65 | RUN ls 66 | ` 67 | ref, cmdline, _, ok = DetectSyntax(bytes.NewBuffer([]byte(dt))) 68 | require.False(t, ok) 69 | require.Equal(t, ref, "") 70 | require.Equal(t, cmdline, "") 71 | 72 | } 73 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/image.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/docker/docker/api/types/strslice" 7 | "github.com/moby/buildkit/util/system" 8 | specs "github.com/opencontainers/image-spec/specs-go/v1" 9 | ) 10 | 11 | // HealthConfig holds configuration settings for the HEALTHCHECK feature. 12 | type HealthConfig struct { 13 | // Test is the test to perform to check that the container is healthy. 14 | // An empty slice means to inherit the default. 15 | // The options are: 16 | // {} : inherit healthcheck 17 | // {"NONE"} : disable healthcheck 18 | // {"CMD", args...} : exec arguments directly 19 | // {"CMD-SHELL", command} : run command with system's default shell 20 | Test []string `json:",omitempty"` 21 | 22 | // Zero means to inherit. Durations are expressed as integer nanoseconds. 23 | Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. 24 | Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. 25 | StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down. 26 | 27 | // Retries is the number of consecutive failures needed to consider a container as unhealthy. 28 | // Zero means inherit. 29 | Retries int `json:",omitempty"` 30 | } 31 | 32 | // ImageConfig is a docker compatible config for an image 33 | type ImageConfig struct { 34 | specs.ImageConfig 35 | 36 | Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy 37 | ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific) 38 | 39 | // NetworkDisabled bool `json:",omitempty"` // Is network disabled 40 | // MacAddress string `json:",omitempty"` // Mac Address of the container 41 | OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile 42 | StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container 43 | Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT 44 | } 45 | 46 | // Image is the JSON structure which describes some basic information about the image. 47 | // This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. 48 | type Image struct { 49 | specs.Image 50 | 51 | // Config defines the execution parameters which should be used as a base when running a container using the image. 52 | Config ImageConfig `json:"config,omitempty"` 53 | 54 | // Variant defines platform variant. To be added to OCI. 55 | Variant string `json:"variant,omitempty"` 56 | } 57 | 58 | func clone(src Image) Image { 59 | img := src 60 | img.Config = src.Config 61 | img.Config.Env = append([]string{}, src.Config.Env...) 62 | img.Config.Cmd = append([]string{}, src.Config.Cmd...) 63 | img.Config.Entrypoint = append([]string{}, src.Config.Entrypoint...) 64 | return img 65 | } 66 | 67 | func emptyImage(platform specs.Platform) Image { 68 | img := Image{ 69 | Image: specs.Image{ 70 | Architecture: platform.Architecture, 71 | OS: platform.OS, 72 | }, 73 | Variant: platform.Variant, 74 | } 75 | img.RootFS.Type = "layers" 76 | img.Config.WorkingDir = "/" 77 | img.Config.Env = []string{"PATH=" + system.DefaultPathEnv(platform.OS)} 78 | return img 79 | } 80 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/platform.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "github.com/agbell/compiling-containers/ickfile/instructions" 5 | "github.com/containerd/containerd/platforms" 6 | specs "github.com/opencontainers/image-spec/specs-go/v1" 7 | ) 8 | 9 | type platformOpt struct { 10 | targetPlatform specs.Platform 11 | buildPlatforms []specs.Platform 12 | implicitTarget bool 13 | } 14 | 15 | func buildPlatformOpt(opt *ConvertOpt) *platformOpt { 16 | buildPlatforms := opt.BuildPlatforms 17 | targetPlatform := opt.TargetPlatform 18 | implicitTargetPlatform := false 19 | 20 | if opt.TargetPlatform != nil && opt.BuildPlatforms == nil { 21 | buildPlatforms = []specs.Platform{*opt.TargetPlatform} 22 | } 23 | if len(buildPlatforms) == 0 { 24 | buildPlatforms = []specs.Platform{platforms.DefaultSpec()} 25 | } 26 | 27 | if opt.TargetPlatform == nil { 28 | implicitTargetPlatform = true 29 | targetPlatform = &buildPlatforms[0] 30 | } 31 | 32 | return &platformOpt{ 33 | targetPlatform: *targetPlatform, 34 | buildPlatforms: buildPlatforms, 35 | implicitTarget: implicitTargetPlatform, 36 | } 37 | } 38 | 39 | func getPlatformArgs(po *platformOpt) []instructions.KeyValuePairOptional { 40 | bp := po.buildPlatforms[0] 41 | tp := po.targetPlatform 42 | m := map[string]string{ 43 | "BUILDPLATFORM": platforms.Format(bp), 44 | "BUILDOS": bp.OS, 45 | "BUILDARCH": bp.Architecture, 46 | "BUILDVARIANT": bp.Variant, 47 | "TARGETPLATFORM": platforms.Format(tp), 48 | "TARGETOS": tp.OS, 49 | "TARGETARCH": tp.Architecture, 50 | "TARGETVARIANT": tp.Variant, 51 | } 52 | opts := make([]instructions.KeyValuePairOptional, 0, len(m)) 53 | for k, v := range m { 54 | s := v 55 | opts = append(opts, instructions.KeyValuePairOptional{Key: k, Value: &s}) 56 | } 57 | return opts 58 | } 59 | -------------------------------------------------------------------------------- /ickfile/dockerfile2llb/platform_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/containerd/containerd/platforms" 7 | specs "github.com/opencontainers/image-spec/specs-go/v1" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestResolveBuildPlatforms(t *testing.T) { 12 | dummyPlatform1 := specs.Platform{Architecture: "DummyArchitecture1", OS: "DummyOS1"} 13 | dummyPlatform2 := specs.Platform{Architecture: "DummyArchitecture2", OS: "DummyOS2"} 14 | 15 | // BuildPlatforms is set and TargetPlatform is set 16 | opt := ConvertOpt{TargetPlatform: &dummyPlatform1, BuildPlatforms: []specs.Platform{dummyPlatform2}} 17 | result := buildPlatformOpt(&opt).buildPlatforms 18 | assert.Equal(t, []specs.Platform{dummyPlatform2}, result) 19 | 20 | // BuildPlatforms is not set and TargetPlatform is set 21 | opt = ConvertOpt{TargetPlatform: &dummyPlatform1, BuildPlatforms: nil} 22 | result = buildPlatformOpt(&opt).buildPlatforms 23 | assert.Equal(t, []specs.Platform{dummyPlatform1}, result) 24 | 25 | // BuildPlatforms is set and TargetPlatform is not set 26 | opt = ConvertOpt{TargetPlatform: nil, BuildPlatforms: []specs.Platform{dummyPlatform2}} 27 | result = buildPlatformOpt(&opt).buildPlatforms 28 | assert.Equal(t, []specs.Platform{dummyPlatform2}, result) 29 | 30 | // BuildPlatforms is not set and TargetPlatform is not set 31 | opt = ConvertOpt{TargetPlatform: nil, BuildPlatforms: nil} 32 | result = buildPlatformOpt(&opt).buildPlatforms 33 | assert.Equal(t, []specs.Platform{platforms.DefaultSpec()}, result) 34 | } 35 | 36 | func TestResolveTargetPlatform(t *testing.T) { 37 | dummyPlatform := specs.Platform{Architecture: "DummyArchitecture", OS: "DummyOS"} 38 | 39 | // TargetPlatform is set 40 | opt := ConvertOpt{TargetPlatform: &dummyPlatform} 41 | result := buildPlatformOpt(&opt) 42 | assert.Equal(t, dummyPlatform, result.targetPlatform) 43 | 44 | // TargetPlatform is not set 45 | opt = ConvertOpt{TargetPlatform: nil} 46 | result = buildPlatformOpt(&opt) 47 | assert.Equal(t, result.buildPlatforms[0], result.targetPlatform) 48 | } 49 | 50 | func TestImplicitTargetPlatform(t *testing.T) { 51 | dummyPlatform := specs.Platform{Architecture: "DummyArchitecture", OS: "DummyOS"} 52 | 53 | // TargetPlatform is set 54 | opt := ConvertOpt{TargetPlatform: &dummyPlatform} 55 | result := buildPlatformOpt(&opt).implicitTarget 56 | assert.Equal(t, false, result) 57 | 58 | // TargetPlatform is not set 59 | opt = ConvertOpt{TargetPlatform: nil} 60 | result = buildPlatformOpt(&opt).implicitTarget 61 | assert.Equal(t, true, result) 62 | } 63 | -------------------------------------------------------------------------------- /ickfile/instructions/bflag.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // FlagType is the type of the build flag 9 | type FlagType int 10 | 11 | const ( 12 | boolType FlagType = iota 13 | stringType 14 | stringsType 15 | ) 16 | 17 | // BFlags contains all flags information for the builder 18 | type BFlags struct { 19 | Args []string // actual flags/args from cmd line 20 | flags map[string]*Flag 21 | used map[string]*Flag 22 | Err error 23 | } 24 | 25 | // Flag contains all information for a flag 26 | type Flag struct { 27 | bf *BFlags 28 | name string 29 | flagType FlagType 30 | Value string 31 | StringValues []string 32 | } 33 | 34 | // NewBFlags returns the new BFlags struct 35 | func NewBFlags() *BFlags { 36 | return &BFlags{ 37 | flags: make(map[string]*Flag), 38 | used: make(map[string]*Flag), 39 | } 40 | } 41 | 42 | // NewBFlagsWithArgs returns the new BFlags struct with Args set to args 43 | func NewBFlagsWithArgs(args []string) *BFlags { 44 | flags := NewBFlags() 45 | flags.Args = args 46 | return flags 47 | } 48 | 49 | // AddBool adds a bool flag to BFlags 50 | // Note, any error will be generated when Parse() is called (see Parse). 51 | func (bf *BFlags) AddBool(name string, def bool) *Flag { 52 | flag := bf.addFlag(name, boolType) 53 | if flag == nil { 54 | return nil 55 | } 56 | if def { 57 | flag.Value = "true" 58 | } else { 59 | flag.Value = "false" 60 | } 61 | return flag 62 | } 63 | 64 | // AddString adds a string flag to BFlags 65 | // Note, any error will be generated when Parse() is called (see Parse). 66 | func (bf *BFlags) AddString(name string, def string) *Flag { 67 | flag := bf.addFlag(name, stringType) 68 | if flag == nil { 69 | return nil 70 | } 71 | flag.Value = def 72 | return flag 73 | } 74 | 75 | // AddStrings adds a string flag to BFlags that can match multiple values 76 | func (bf *BFlags) AddStrings(name string) *Flag { 77 | flag := bf.addFlag(name, stringsType) 78 | if flag == nil { 79 | return nil 80 | } 81 | return flag 82 | } 83 | 84 | // addFlag is a generic func used by the other AddXXX() func 85 | // to add a new flag to the BFlags struct. 86 | // Note, any error will be generated when Parse() is called (see Parse). 87 | func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag { 88 | if _, ok := bf.flags[name]; ok { 89 | bf.Err = fmt.Errorf("Duplicate flag defined: %s", name) 90 | return nil 91 | } 92 | 93 | newFlag := &Flag{ 94 | bf: bf, 95 | name: name, 96 | flagType: flagType, 97 | } 98 | bf.flags[name] = newFlag 99 | 100 | return newFlag 101 | } 102 | 103 | // IsUsed checks if the flag is used 104 | func (fl *Flag) IsUsed() bool { 105 | if _, ok := fl.bf.used[fl.name]; ok { 106 | return true 107 | } 108 | return false 109 | } 110 | 111 | // IsTrue checks if a bool flag is true 112 | func (fl *Flag) IsTrue() bool { 113 | if fl.flagType != boolType { 114 | // Should never get here 115 | panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name)) 116 | } 117 | return fl.Value == "true" 118 | } 119 | 120 | // Parse parses and checks if the BFlags is valid. 121 | // Any error noticed during the AddXXX() funcs will be generated/returned 122 | // here. We do this because an error during AddXXX() is more like a 123 | // compile time error so it doesn't matter too much when we stop our 124 | // processing as long as we do stop it, so this allows the code 125 | // around AddXXX() to be just: 126 | // defFlag := AddString("description", "") 127 | // w/o needing to add an if-statement around each one. 128 | func (bf *BFlags) Parse() error { 129 | // If there was an error while defining the possible flags 130 | // go ahead and bubble it back up here since we didn't do it 131 | // earlier in the processing 132 | if bf.Err != nil { 133 | return fmt.Errorf("Error setting up flags: %s", bf.Err) 134 | } 135 | 136 | for _, arg := range bf.Args { 137 | if !strings.HasPrefix(arg, "--") { 138 | return fmt.Errorf("Arg should start with -- : %s", arg) 139 | } 140 | 141 | if arg == "--" { 142 | return nil 143 | } 144 | 145 | arg = arg[2:] 146 | value := "" 147 | 148 | index := strings.Index(arg, "=") 149 | if index >= 0 { 150 | value = arg[index+1:] 151 | arg = arg[:index] 152 | } 153 | 154 | flag, ok := bf.flags[arg] 155 | if !ok { 156 | return fmt.Errorf("Unknown flag: %s", arg) 157 | } 158 | 159 | if _, ok = bf.used[arg]; ok && flag.flagType != stringsType { 160 | return fmt.Errorf("Duplicate flag specified: %s", arg) 161 | } 162 | 163 | bf.used[arg] = flag 164 | 165 | switch flag.flagType { 166 | case boolType: 167 | // value == "" is only ok if no "=" was specified 168 | if index >= 0 && value == "" { 169 | return fmt.Errorf("Missing a value on flag: %s", arg) 170 | } 171 | 172 | lower := strings.ToLower(value) 173 | if lower == "" { 174 | flag.Value = "true" 175 | } else if lower == "true" || lower == "false" { 176 | flag.Value = lower 177 | } else { 178 | return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value) 179 | } 180 | 181 | case stringType: 182 | if index < 0 { 183 | return fmt.Errorf("Missing a value on flag: %s", arg) 184 | } 185 | flag.Value = value 186 | 187 | case stringsType: 188 | if index < 0 { 189 | return fmt.Errorf("Missing a value on flag: %s", arg) 190 | } 191 | flag.StringValues = append(flag.StringValues, value) 192 | 193 | default: 194 | panic("No idea what kind of flag we have! Should never get here!") 195 | } 196 | 197 | } 198 | 199 | return nil 200 | } 201 | -------------------------------------------------------------------------------- /ickfile/instructions/bflag_test.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBuilderFlags(t *testing.T) { 8 | var expected string 9 | var err error 10 | 11 | // --- 12 | 13 | bf := NewBFlags() 14 | bf.Args = []string{} 15 | if err := bf.Parse(); err != nil { 16 | t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err) 17 | } 18 | 19 | // --- 20 | 21 | bf = NewBFlags() 22 | bf.Args = []string{"--"} 23 | if err := bf.Parse(); err != nil { 24 | t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err) 25 | } 26 | 27 | // --- 28 | 29 | bf = NewBFlags() 30 | flStr1 := bf.AddString("str1", "") 31 | flBool1 := bf.AddBool("bool1", false) 32 | bf.Args = []string{} 33 | if err = bf.Parse(); err != nil { 34 | t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err) 35 | } 36 | 37 | if flStr1.IsUsed() { 38 | t.Fatal("Test3 - str1 was not used!") 39 | } 40 | if flBool1.IsUsed() { 41 | t.Fatal("Test3 - bool1 was not used!") 42 | } 43 | 44 | // --- 45 | 46 | bf = NewBFlags() 47 | flStr1 = bf.AddString("str1", "HI") 48 | flBool1 = bf.AddBool("bool1", false) 49 | bf.Args = []string{} 50 | 51 | if err = bf.Parse(); err != nil { 52 | t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err) 53 | } 54 | 55 | if flStr1.Value != "HI" { 56 | t.Fatal("Str1 was supposed to default to: HI") 57 | } 58 | if flBool1.IsTrue() { 59 | t.Fatal("Bool1 was supposed to default to: false") 60 | } 61 | if flStr1.IsUsed() { 62 | t.Fatal("Str1 was not used!") 63 | } 64 | if flBool1.IsUsed() { 65 | t.Fatal("Bool1 was not used!") 66 | } 67 | 68 | // --- 69 | 70 | bf = NewBFlags() 71 | bf.AddString("str1", "HI") 72 | bf.Args = []string{"--str1"} 73 | 74 | if err = bf.Parse(); err == nil { 75 | t.Fatalf("Test %q was supposed to fail", bf.Args) 76 | } 77 | 78 | // --- 79 | 80 | bf = NewBFlags() 81 | flStr1 = bf.AddString("str1", "HI") 82 | bf.Args = []string{"--str1="} 83 | 84 | if err = bf.Parse(); err != nil { 85 | t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) 86 | } 87 | 88 | expected = "" 89 | if flStr1.Value != expected { 90 | t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) 91 | } 92 | 93 | // --- 94 | 95 | bf = NewBFlags() 96 | flStr1 = bf.AddString("str1", "HI") 97 | bf.Args = []string{"--str1=BYE"} 98 | 99 | if err = bf.Parse(); err != nil { 100 | t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) 101 | } 102 | 103 | expected = "BYE" 104 | if flStr1.Value != expected { 105 | t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) 106 | } 107 | 108 | // --- 109 | 110 | bf = NewBFlags() 111 | flBool1 = bf.AddBool("bool1", false) 112 | bf.Args = []string{"--bool1"} 113 | 114 | if err = bf.Parse(); err != nil { 115 | t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) 116 | } 117 | 118 | if !flBool1.IsTrue() { 119 | t.Fatal("Test-b1 Bool1 was supposed to be true") 120 | } 121 | 122 | // --- 123 | 124 | bf = NewBFlags() 125 | flBool1 = bf.AddBool("bool1", false) 126 | bf.Args = []string{"--bool1=true"} 127 | 128 | if err = bf.Parse(); err != nil { 129 | t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) 130 | } 131 | 132 | if !flBool1.IsTrue() { 133 | t.Fatal("Test-b2 Bool1 was supposed to be true") 134 | } 135 | 136 | // --- 137 | 138 | bf = NewBFlags() 139 | flBool1 = bf.AddBool("bool1", false) 140 | bf.Args = []string{"--bool1=false"} 141 | 142 | if err = bf.Parse(); err != nil { 143 | t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) 144 | } 145 | 146 | if flBool1.IsTrue() { 147 | t.Fatal("Test-b3 Bool1 was supposed to be false") 148 | } 149 | 150 | // --- 151 | 152 | bf = NewBFlags() 153 | bf.AddBool("bool1", false) 154 | bf.Args = []string{"--bool1=false1"} 155 | 156 | if err = bf.Parse(); err == nil { 157 | t.Fatalf("Test %q was supposed to fail", bf.Args) 158 | } 159 | 160 | // --- 161 | 162 | bf = NewBFlags() 163 | bf.AddBool("bool1", false) 164 | bf.Args = []string{"--bool2"} 165 | 166 | if err = bf.Parse(); err == nil { 167 | t.Fatalf("Test %q was supposed to fail", bf.Args) 168 | } 169 | 170 | // --- 171 | 172 | bf = NewBFlags() 173 | flStr1 = bf.AddString("str1", "HI") 174 | flBool1 = bf.AddBool("bool1", false) 175 | bf.Args = []string{"--bool1", "--str1=BYE"} 176 | 177 | if err = bf.Parse(); err != nil { 178 | t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) 179 | } 180 | 181 | if flStr1.Value != "BYE" { 182 | t.Fatalf("Test %s, str1 should be BYE", bf.Args) 183 | } 184 | if !flBool1.IsTrue() { 185 | t.Fatalf("Test %s, bool1 should be true", bf.Args) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /ickfile/instructions/commands.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/agbell/compiling-containers/ickfile/parser" 7 | "github.com/docker/docker/api/types/container" 8 | "github.com/docker/docker/api/types/strslice" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // KeyValuePair represent an arbitrary named value (useful in slice instead of map[string] string to preserve ordering) 13 | type KeyValuePair struct { 14 | Key string 15 | Value string 16 | } 17 | 18 | func (kvp *KeyValuePair) String() string { 19 | return kvp.Key + "=" + kvp.Value 20 | } 21 | 22 | // KeyValuePairOptional is the same as KeyValuePair but Value is optional 23 | type KeyValuePairOptional struct { 24 | Key string 25 | Value *string 26 | Comment string 27 | } 28 | 29 | func (kvpo *KeyValuePairOptional) ValueString() string { 30 | v := "" 31 | if kvpo.Value != nil { 32 | v = *kvpo.Value 33 | } 34 | return v 35 | } 36 | 37 | // Command is implemented by every command present in a dockerfile 38 | type Command interface { 39 | Name() string 40 | Location() []parser.Range 41 | } 42 | 43 | // KeyValuePairs is a slice of KeyValuePair 44 | type KeyValuePairs []KeyValuePair 45 | 46 | // withNameAndCode is the base of every command in a Dockerfile (String() returns its source code) 47 | type withNameAndCode struct { 48 | code string 49 | name string 50 | location []parser.Range 51 | } 52 | 53 | func (c *withNameAndCode) String() string { 54 | return c.code 55 | } 56 | 57 | // Name of the command 58 | func (c *withNameAndCode) Name() string { 59 | return c.name 60 | } 61 | 62 | // Location of the command in source 63 | func (c *withNameAndCode) Location() []parser.Range { 64 | return c.location 65 | } 66 | 67 | func newWithNameAndCode(req parseRequest) withNameAndCode { 68 | return withNameAndCode{code: strings.TrimSpace(req.original), name: req.command, location: req.location} 69 | } 70 | 71 | // SingleWordExpander is a provider for variable expansion where 1 word => 1 output 72 | type SingleWordExpander func(word string) (string, error) 73 | 74 | // SupportsSingleWordExpansion interface marks a command as supporting variable expansion 75 | type SupportsSingleWordExpansion interface { 76 | Expand(expander SingleWordExpander) error 77 | } 78 | 79 | // PlatformSpecific adds platform checks to a command 80 | type PlatformSpecific interface { 81 | CheckPlatform(platform string) error 82 | } 83 | 84 | func expandKvp(kvp KeyValuePair, expander SingleWordExpander) (KeyValuePair, error) { 85 | key, err := expander(kvp.Key) 86 | if err != nil { 87 | return KeyValuePair{}, err 88 | } 89 | value, err := expander(kvp.Value) 90 | if err != nil { 91 | return KeyValuePair{}, err 92 | } 93 | return KeyValuePair{Key: key, Value: value}, nil 94 | } 95 | func expandKvpsInPlace(kvps KeyValuePairs, expander SingleWordExpander) error { 96 | for i, kvp := range kvps { 97 | newKvp, err := expandKvp(kvp, expander) 98 | if err != nil { 99 | return err 100 | } 101 | kvps[i] = newKvp 102 | } 103 | return nil 104 | } 105 | 106 | func expandSliceInPlace(values []string, expander SingleWordExpander) error { 107 | for i, v := range values { 108 | newValue, err := expander(v) 109 | if err != nil { 110 | return err 111 | } 112 | values[i] = newValue 113 | } 114 | return nil 115 | } 116 | 117 | // EnvCommand : ENV key1 value1 [keyN valueN...] 118 | type EnvCommand struct { 119 | withNameAndCode 120 | Env KeyValuePairs // kvp slice instead of map to preserve ordering 121 | } 122 | 123 | // Expand variables 124 | func (c *EnvCommand) Expand(expander SingleWordExpander) error { 125 | return expandKvpsInPlace(c.Env, expander) 126 | } 127 | 128 | // MaintainerCommand : MAINTAINER maintainer_name 129 | type MaintainerCommand struct { 130 | withNameAndCode 131 | Maintainer string 132 | } 133 | 134 | // NewLabelCommand creates a new 'LABEL' command 135 | func NewLabelCommand(k string, v string, NoExp bool) *LabelCommand { 136 | kvp := KeyValuePair{Key: k, Value: v} 137 | c := "LABEL " 138 | c += kvp.String() 139 | nc := withNameAndCode{code: c, name: "label"} 140 | cmd := &LabelCommand{ 141 | withNameAndCode: nc, 142 | Labels: KeyValuePairs{ 143 | kvp, 144 | }, 145 | noExpand: NoExp, 146 | } 147 | return cmd 148 | } 149 | 150 | // LabelCommand : LABEL some json data describing the image 151 | // 152 | // Sets the Label variable foo to bar, 153 | // 154 | type LabelCommand struct { 155 | withNameAndCode 156 | Labels KeyValuePairs // kvp slice instead of map to preserve ordering 157 | noExpand bool 158 | } 159 | 160 | // Expand variables 161 | func (c *LabelCommand) Expand(expander SingleWordExpander) error { 162 | if c.noExpand { 163 | return nil 164 | } 165 | return expandKvpsInPlace(c.Labels, expander) 166 | } 167 | 168 | // SourcesAndDest represent a list of source files and a destination 169 | type SourcesAndDest []string 170 | 171 | // Sources list the source paths 172 | func (s SourcesAndDest) Sources() []string { 173 | res := make([]string, len(s)-1) 174 | copy(res, s[:len(s)-1]) 175 | return res 176 | } 177 | 178 | // Dest path of the operation 179 | func (s SourcesAndDest) Dest() string { 180 | return s[len(s)-1] 181 | } 182 | 183 | // AddCommand : ADD foo /path 184 | // 185 | // Add the file 'foo' to '/path'. Tarball and Remote URL (http, https) handling 186 | // exist here. If you do not wish to have this automatic handling, use COPY. 187 | // 188 | type AddCommand struct { 189 | withNameAndCode 190 | SourcesAndDest 191 | Chown string 192 | Chmod string 193 | } 194 | 195 | // Expand variables 196 | func (c *AddCommand) Expand(expander SingleWordExpander) error { 197 | expandedChown, err := expander(c.Chown) 198 | if err != nil { 199 | return err 200 | } 201 | c.Chown = expandedChown 202 | return expandSliceInPlace(c.SourcesAndDest, expander) 203 | } 204 | 205 | // CopyCommand : COPY foo /path 206 | // 207 | // Same as 'ADD' but without the tar and remote url handling. 208 | // 209 | type CopyCommand struct { 210 | withNameAndCode 211 | SourcesAndDest 212 | From string 213 | Chown string 214 | Chmod string 215 | } 216 | 217 | // Expand variables 218 | func (c *CopyCommand) Expand(expander SingleWordExpander) error { 219 | expandedChown, err := expander(c.Chown) 220 | if err != nil { 221 | return err 222 | } 223 | c.Chown = expandedChown 224 | return expandSliceInPlace(c.SourcesAndDest, expander) 225 | } 226 | 227 | // OnbuildCommand : ONBUILD 228 | type OnbuildCommand struct { 229 | withNameAndCode 230 | Expression string 231 | } 232 | 233 | // WorkdirCommand : WORKDIR /tmp 234 | // 235 | // Set the working directory for future RUN/CMD/etc statements. 236 | // 237 | type WorkdirCommand struct { 238 | withNameAndCode 239 | Path string 240 | } 241 | 242 | // Expand variables 243 | func (c *WorkdirCommand) Expand(expander SingleWordExpander) error { 244 | p, err := expander(c.Path) 245 | if err != nil { 246 | return err 247 | } 248 | c.Path = p 249 | return nil 250 | } 251 | 252 | // ShellDependantCmdLine represents a cmdline optionally prepended with the shell 253 | type ShellDependantCmdLine struct { 254 | CmdLine strslice.StrSlice 255 | PrependShell bool 256 | } 257 | 258 | // RunCommand : RUN some command yo 259 | // 260 | // run a command and commit the image. Args are automatically prepended with 261 | // the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under 262 | // Windows, in the event there is only one argument The difference in processing: 263 | // 264 | // RUN echo hi # sh -c echo hi (Linux) 265 | // RUN echo hi # cmd /S /C echo hi (Windows) 266 | // RUN [ "echo", "hi" ] # echo hi 267 | // 268 | type RunCommand struct { 269 | withNameAndCode 270 | withExternalData 271 | ShellDependantCmdLine 272 | } 273 | 274 | // CmdCommand : CMD foo 275 | // 276 | // Set the default command to run in the container (which may be empty). 277 | // Argument handling is the same as RUN. 278 | // 279 | type CmdCommand struct { 280 | withNameAndCode 281 | ShellDependantCmdLine 282 | } 283 | 284 | // HealthCheckCommand : HEALTHCHECK foo 285 | // 286 | // Set the default healthcheck command to run in the container (which may be empty). 287 | // Argument handling is the same as RUN. 288 | // 289 | type HealthCheckCommand struct { 290 | withNameAndCode 291 | Health *container.HealthConfig 292 | } 293 | 294 | // EntrypointCommand : ENTRYPOINT /usr/sbin/nginx 295 | // 296 | // Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments 297 | // to /usr/sbin/nginx. Uses the default shell if not in JSON format. 298 | // 299 | // Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint 300 | // is initialized at newBuilder time instead of through argument parsing. 301 | // 302 | type EntrypointCommand struct { 303 | withNameAndCode 304 | ShellDependantCmdLine 305 | } 306 | 307 | // ExposeCommand : EXPOSE 6667/tcp 7000/tcp 308 | // 309 | // Expose ports for links and port mappings. This all ends up in 310 | // req.runConfig.ExposedPorts for runconfig. 311 | // 312 | type ExposeCommand struct { 313 | withNameAndCode 314 | Ports []string 315 | } 316 | 317 | // UserCommand : USER foo 318 | // 319 | // Set the user to 'foo' for future commands and when running the 320 | // ENTRYPOINT/CMD at container run time. 321 | // 322 | type UserCommand struct { 323 | withNameAndCode 324 | User string 325 | } 326 | 327 | // Expand variables 328 | func (c *UserCommand) Expand(expander SingleWordExpander) error { 329 | p, err := expander(c.User) 330 | if err != nil { 331 | return err 332 | } 333 | c.User = p 334 | return nil 335 | } 336 | 337 | // VolumeCommand : VOLUME /foo 338 | // 339 | // Expose the volume /foo for use. Will also accept the JSON array form. 340 | // 341 | type VolumeCommand struct { 342 | withNameAndCode 343 | Volumes []string 344 | } 345 | 346 | // Expand variables 347 | func (c *VolumeCommand) Expand(expander SingleWordExpander) error { 348 | return expandSliceInPlace(c.Volumes, expander) 349 | } 350 | 351 | // StopSignalCommand : STOPSIGNAL signal 352 | // 353 | // Set the signal that will be used to kill the container. 354 | type StopSignalCommand struct { 355 | withNameAndCode 356 | Signal string 357 | } 358 | 359 | // Expand variables 360 | func (c *StopSignalCommand) Expand(expander SingleWordExpander) error { 361 | p, err := expander(c.Signal) 362 | if err != nil { 363 | return err 364 | } 365 | c.Signal = p 366 | return nil 367 | } 368 | 369 | // CheckPlatform checks that the command is supported in the target platform 370 | func (c *StopSignalCommand) CheckPlatform(platform string) error { 371 | if platform == "windows" { 372 | return errors.New("The daemon on this platform does not support the command stopsignal") 373 | } 374 | return nil 375 | } 376 | 377 | // ArgCommand : ARG name[=value] 378 | // 379 | // Adds the variable foo to the trusted list of variables that can be passed 380 | // to builder using the --build-arg flag for expansion/substitution or passing to 'run'. 381 | // Dockerfile author may optionally set a default value of this variable. 382 | type ArgCommand struct { 383 | withNameAndCode 384 | Args []KeyValuePairOptional 385 | } 386 | 387 | // Expand variables 388 | func (c *ArgCommand) Expand(expander SingleWordExpander) error { 389 | for i, v := range c.Args { 390 | p, err := expander(v.Key) 391 | if err != nil { 392 | return err 393 | } 394 | v.Key = p 395 | if v.Value != nil { 396 | p, err = expander(*v.Value) 397 | if err != nil { 398 | return err 399 | } 400 | v.Value = &p 401 | } 402 | c.Args[i] = v 403 | } 404 | return nil 405 | } 406 | 407 | // ShellCommand : SHELL powershell -command 408 | // 409 | // Set the non-default shell to use. 410 | type ShellCommand struct { 411 | withNameAndCode 412 | Shell strslice.StrSlice 413 | } 414 | 415 | // Stage represents a single stage in a multi-stage build 416 | type Stage struct { 417 | Name string 418 | Commands []Command 419 | BaseName string 420 | SourceCode string 421 | Platform string 422 | Location []parser.Range 423 | Comment string 424 | } 425 | 426 | // AddCommand to the stage 427 | func (s *Stage) AddCommand(cmd Command) { 428 | // todo: validate cmd type 429 | s.Commands = append(s.Commands, cmd) 430 | } 431 | 432 | // IsCurrentStage check if the stage name is the current stage 433 | func IsCurrentStage(s []Stage, name string) bool { 434 | if len(s) == 0 { 435 | return false 436 | } 437 | return s[len(s)-1].Name == name 438 | } 439 | 440 | // CurrentStage return the last stage in a slice 441 | func CurrentStage(s []Stage) (*Stage, error) { 442 | if len(s) == 0 { 443 | return nil, errors.New("no build stage in current context") 444 | } 445 | return &s[len(s)-1], nil 446 | } 447 | 448 | // HasStage looks for the presence of a given stage name 449 | func HasStage(s []Stage, name string) (int, bool) { 450 | for i, stage := range s { 451 | // Stage name is case-insensitive by design 452 | if strings.EqualFold(stage.Name, name) { 453 | return i, true 454 | } 455 | } 456 | return -1, false 457 | } 458 | 459 | type withExternalData struct { 460 | m map[interface{}]interface{} 461 | } 462 | 463 | func (c *withExternalData) getExternalValue(k interface{}) interface{} { 464 | return c.m[k] 465 | } 466 | 467 | func (c *withExternalData) setExternalValue(k, v interface{}) { 468 | if c.m == nil { 469 | c.m = map[interface{}]interface{}{} 470 | } 471 | c.m[k] = v 472 | } 473 | -------------------------------------------------------------------------------- /ickfile/instructions/commands_runmount.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "encoding/csv" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const MountTypeBind = "bind" 12 | const MountTypeCache = "cache" 13 | const MountTypeTmpfs = "tmpfs" 14 | const MountTypeSecret = "secret" 15 | const MountTypeSSH = "ssh" 16 | 17 | var allowedMountTypes = map[string]struct{}{ 18 | MountTypeBind: {}, 19 | MountTypeCache: {}, 20 | MountTypeTmpfs: {}, 21 | MountTypeSecret: {}, 22 | MountTypeSSH: {}, 23 | } 24 | 25 | const MountSharingShared = "shared" 26 | const MountSharingPrivate = "private" 27 | const MountSharingLocked = "locked" 28 | 29 | var allowedSharingTypes = map[string]struct{}{ 30 | MountSharingShared: {}, 31 | MountSharingPrivate: {}, 32 | MountSharingLocked: {}, 33 | } 34 | 35 | type mountsKeyT string 36 | 37 | var mountsKey = mountsKeyT("dockerfile/run/mounts") 38 | 39 | func init() { 40 | parseRunPreHooks = append(parseRunPreHooks, runMountPreHook) 41 | parseRunPostHooks = append(parseRunPostHooks, runMountPostHook) 42 | } 43 | 44 | func isValidMountType(s string) bool { 45 | if s == "secret" { 46 | if !isSecretMountsSupported() { 47 | return false 48 | } 49 | } 50 | if s == "ssh" { 51 | if !isSSHMountsSupported() { 52 | return false 53 | } 54 | } 55 | _, ok := allowedMountTypes[s] 56 | return ok 57 | } 58 | 59 | func runMountPreHook(cmd *RunCommand, req parseRequest) error { 60 | st := &mountState{} 61 | st.flag = req.flags.AddStrings("mount") 62 | cmd.setExternalValue(mountsKey, st) 63 | return nil 64 | } 65 | 66 | func runMountPostHook(cmd *RunCommand, req parseRequest) error { 67 | st := getMountState(cmd) 68 | if st == nil { 69 | return errors.Errorf("no mount state") 70 | } 71 | var mounts []*Mount 72 | for _, str := range st.flag.StringValues { 73 | m, err := parseMount(str) 74 | if err != nil { 75 | return err 76 | } 77 | mounts = append(mounts, m) 78 | } 79 | st.mounts = mounts 80 | return nil 81 | } 82 | 83 | func getMountState(cmd *RunCommand) *mountState { 84 | v := cmd.getExternalValue(mountsKey) 85 | if v == nil { 86 | return nil 87 | } 88 | return v.(*mountState) 89 | } 90 | 91 | func GetMounts(cmd *RunCommand) []*Mount { 92 | return getMountState(cmd).mounts 93 | } 94 | 95 | type mountState struct { 96 | flag *Flag 97 | mounts []*Mount 98 | } 99 | 100 | type Mount struct { 101 | Type string 102 | From string 103 | Source string 104 | Target string 105 | ReadOnly bool 106 | CacheID string 107 | CacheSharing string 108 | Required bool 109 | Mode *uint64 110 | UID *uint64 111 | GID *uint64 112 | } 113 | 114 | func parseMount(value string) (*Mount, error) { 115 | csvReader := csv.NewReader(strings.NewReader(value)) 116 | fields, err := csvReader.Read() 117 | if err != nil { 118 | return nil, errors.Wrap(err, "failed to parse csv mounts") 119 | } 120 | 121 | m := &Mount{Type: MountTypeBind} 122 | 123 | roAuto := true 124 | 125 | for _, field := range fields { 126 | parts := strings.SplitN(field, "=", 2) 127 | key := strings.ToLower(parts[0]) 128 | 129 | if len(parts) == 1 { 130 | switch key { 131 | case "readonly", "ro": 132 | m.ReadOnly = true 133 | roAuto = false 134 | continue 135 | case "readwrite", "rw": 136 | m.ReadOnly = false 137 | roAuto = false 138 | continue 139 | case "required": 140 | if m.Type == "secret" || m.Type == "ssh" { 141 | m.Required = true 142 | continue 143 | } else { 144 | return nil, errors.Errorf("unexpected key '%s' for mount type '%s'", key, m.Type) 145 | } 146 | } 147 | } 148 | 149 | if len(parts) != 2 { 150 | return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) 151 | } 152 | 153 | value := parts[1] 154 | switch key { 155 | case "type": 156 | if !isValidMountType(strings.ToLower(value)) { 157 | return nil, errors.Errorf("unsupported mount type %q", value) 158 | } 159 | m.Type = strings.ToLower(value) 160 | case "from": 161 | m.From = value 162 | case "source", "src": 163 | m.Source = value 164 | case "target", "dst", "destination": 165 | m.Target = value 166 | case "readonly", "ro": 167 | m.ReadOnly, err = strconv.ParseBool(value) 168 | if err != nil { 169 | return nil, errors.Errorf("invalid value for %s: %s", key, value) 170 | } 171 | roAuto = false 172 | case "readwrite", "rw": 173 | rw, err := strconv.ParseBool(value) 174 | if err != nil { 175 | return nil, errors.Errorf("invalid value for %s: %s", key, value) 176 | } 177 | m.ReadOnly = !rw 178 | roAuto = false 179 | case "required": 180 | if m.Type == "secret" || m.Type == "ssh" { 181 | v, err := strconv.ParseBool(value) 182 | if err != nil { 183 | return nil, errors.Errorf("invalid value for %s: %s", key, value) 184 | } 185 | m.Required = v 186 | } else { 187 | return nil, errors.Errorf("unexpected key '%s' for mount type '%s'", key, m.Type) 188 | } 189 | case "id": 190 | m.CacheID = value 191 | case "sharing": 192 | if _, ok := allowedSharingTypes[strings.ToLower(value)]; !ok { 193 | return nil, errors.Errorf("unsupported sharing value %q", value) 194 | } 195 | m.CacheSharing = strings.ToLower(value) 196 | case "mode": 197 | mode, err := strconv.ParseUint(value, 8, 32) 198 | if err != nil { 199 | return nil, errors.Errorf("invalid value %s for mode", value) 200 | } 201 | m.Mode = &mode 202 | case "uid": 203 | uid, err := strconv.ParseUint(value, 10, 32) 204 | if err != nil { 205 | return nil, errors.Errorf("invalid value %s for uid", value) 206 | } 207 | m.UID = &uid 208 | case "gid": 209 | gid, err := strconv.ParseUint(value, 10, 32) 210 | if err != nil { 211 | return nil, errors.Errorf("invalid value %s for gid", value) 212 | } 213 | m.GID = &gid 214 | default: 215 | return nil, errors.Errorf("unexpected key '%s' in '%s'", key, field) 216 | } 217 | } 218 | 219 | fileInfoAllowed := m.Type == MountTypeSecret || m.Type == MountTypeSSH || m.Type == MountTypeCache 220 | 221 | if m.Mode != nil && !fileInfoAllowed { 222 | return nil, errors.Errorf("mode not allowed for %q type mounts", m.Type) 223 | } 224 | 225 | if m.UID != nil && !fileInfoAllowed { 226 | return nil, errors.Errorf("uid not allowed for %q type mounts", m.Type) 227 | } 228 | 229 | if m.GID != nil && !fileInfoAllowed { 230 | return nil, errors.Errorf("gid not allowed for %q type mounts", m.Type) 231 | } 232 | 233 | if roAuto { 234 | if m.Type == MountTypeCache || m.Type == MountTypeTmpfs { 235 | m.ReadOnly = false 236 | } else { 237 | m.ReadOnly = true 238 | } 239 | } 240 | 241 | if m.CacheSharing != "" && m.Type != MountTypeCache { 242 | return nil, errors.Errorf("invalid cache sharing set for %v mount", m.Type) 243 | } 244 | 245 | if m.Type == MountTypeSecret { 246 | if m.From != "" { 247 | return nil, errors.Errorf("secret mount should not have a from") 248 | } 249 | if m.CacheSharing != "" { 250 | return nil, errors.Errorf("secret mount should not define sharing") 251 | } 252 | if m.Source == "" && m.Target == "" && m.CacheID == "" { 253 | return nil, errors.Errorf("invalid secret mount. one of source, target required") 254 | } 255 | if m.Source != "" && m.CacheID != "" { 256 | return nil, errors.Errorf("both source and id can't be set") 257 | } 258 | } 259 | 260 | return m, nil 261 | } 262 | -------------------------------------------------------------------------------- /ickfile/instructions/commands_runnetwork.go: -------------------------------------------------------------------------------- 1 | // +build dfrunnetwork 2 | 3 | package instructions 4 | 5 | import ( 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | const ( 10 | NetworkDefault = "default" 11 | NetworkNone = "none" 12 | NetworkHost = "host" 13 | ) 14 | 15 | var allowedNetwork = map[string]struct{}{ 16 | NetworkDefault: {}, 17 | NetworkNone: {}, 18 | NetworkHost: {}, 19 | } 20 | 21 | func isValidNetwork(value string) bool { 22 | _, ok := allowedNetwork[value] 23 | return ok 24 | } 25 | 26 | var networkKey = "dockerfile/run/network" 27 | 28 | func init() { 29 | parseRunPreHooks = append(parseRunPreHooks, runNetworkPreHook) 30 | parseRunPostHooks = append(parseRunPostHooks, runNetworkPostHook) 31 | } 32 | 33 | func runNetworkPreHook(cmd *RunCommand, req parseRequest) error { 34 | st := &networkState{} 35 | st.flag = req.flags.AddString("network", NetworkDefault) 36 | cmd.setExternalValue(networkKey, st) 37 | return nil 38 | } 39 | 40 | func runNetworkPostHook(cmd *RunCommand, req parseRequest) error { 41 | st := cmd.getExternalValue(networkKey).(*networkState) 42 | if st == nil { 43 | return errors.Errorf("no network state") 44 | } 45 | 46 | value := st.flag.Value 47 | if !isValidNetwork(value) { 48 | return errors.Errorf("invalid network mode %q", value) 49 | } 50 | 51 | st.networkMode = value 52 | 53 | return nil 54 | } 55 | 56 | func GetNetwork(cmd *RunCommand) string { 57 | return cmd.getExternalValue(networkKey).(*networkState).networkMode 58 | } 59 | 60 | type networkState struct { 61 | flag *Flag 62 | networkMode string 63 | } 64 | -------------------------------------------------------------------------------- /ickfile/instructions/commands_runsecurity.go: -------------------------------------------------------------------------------- 1 | // +build dfrunsecurity 2 | 3 | package instructions 4 | 5 | import ( 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | const ( 10 | SecurityInsecure = "insecure" 11 | SecuritySandbox = "sandbox" 12 | ) 13 | 14 | var allowedSecurity = map[string]struct{}{ 15 | SecurityInsecure: {}, 16 | SecuritySandbox: {}, 17 | } 18 | 19 | func isValidSecurity(value string) bool { 20 | _, ok := allowedSecurity[value] 21 | return ok 22 | } 23 | 24 | var securityKey = "dockerfile/run/security" 25 | 26 | func init() { 27 | parseRunPreHooks = append(parseRunPreHooks, runSecurityPreHook) 28 | parseRunPostHooks = append(parseRunPostHooks, runSecurityPostHook) 29 | } 30 | 31 | func runSecurityPreHook(cmd *RunCommand, req parseRequest) error { 32 | st := &securityState{} 33 | st.flag = req.flags.AddString("security", SecuritySandbox) 34 | cmd.setExternalValue(securityKey, st) 35 | return nil 36 | } 37 | 38 | func runSecurityPostHook(cmd *RunCommand, req parseRequest) error { 39 | st := cmd.getExternalValue(securityKey).(*securityState) 40 | if st == nil { 41 | return errors.Errorf("no security state") 42 | } 43 | 44 | value := st.flag.Value 45 | if !isValidSecurity(value) { 46 | return errors.Errorf("security %q is not valid", value) 47 | } 48 | 49 | st.security = value 50 | 51 | return nil 52 | } 53 | 54 | func GetSecurity(cmd *RunCommand) string { 55 | return cmd.getExternalValue(securityKey).(*securityState).security 56 | } 57 | 58 | type securityState struct { 59 | flag *Flag 60 | security string 61 | } 62 | -------------------------------------------------------------------------------- /ickfile/instructions/commands_secrets.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | func isSecretMountsSupported() bool { 4 | return true 5 | } 6 | -------------------------------------------------------------------------------- /ickfile/instructions/commands_ssh.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | func isSSHMountsSupported() bool { 4 | return true 5 | } 6 | -------------------------------------------------------------------------------- /ickfile/instructions/errors_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package instructions 4 | 5 | import "fmt" 6 | 7 | func errNotJSON(command, _ string) error { 8 | return fmt.Errorf("%s requires the arguments to be in JSON form", command) 9 | } 10 | -------------------------------------------------------------------------------- /ickfile/instructions/errors_windows.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func errNotJSON(command, original string) error { 11 | // For Windows users, give a hint if it looks like it might contain 12 | // a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"], 13 | // as JSON must be escaped. Unfortunate... 14 | // 15 | // Specifically looking for quote-driveletter-colon-backslash, there's no 16 | // double backslash and a [] pair. No, this is not perfect, but it doesn't 17 | // have to be. It's simply a hint to make life a little easier. 18 | extra := "" 19 | original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1))) 20 | if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 && 21 | !strings.Contains(original, `\\`) && 22 | strings.Contains(original, "[") && 23 | strings.Contains(original, "]") { 24 | extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original) 25 | } 26 | return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra) 27 | } 28 | -------------------------------------------------------------------------------- /ickfile/instructions/parse.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/agbell/compiling-containers/ickfile/command" 12 | "github.com/agbell/compiling-containers/ickfile/parser" 13 | "github.com/docker/docker/api/types/container" 14 | "github.com/docker/docker/api/types/strslice" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | type parseRequest struct { 19 | command string 20 | args []string 21 | attributes map[string]bool 22 | flags *BFlags 23 | original string 24 | location []parser.Range 25 | comments []string 26 | } 27 | 28 | var parseRunPreHooks []func(*RunCommand, parseRequest) error 29 | var parseRunPostHooks []func(*RunCommand, parseRequest) error 30 | 31 | func nodeArgs(node *parser.Node) []string { 32 | result := []string{} 33 | for ; node.Next != nil; node = node.Next { 34 | arg := node.Next 35 | if len(arg.Children) == 0 { 36 | result = append(result, arg.Value) 37 | } else if len(arg.Children) == 1 { 38 | //sub command 39 | result = append(result, arg.Children[0].Value) 40 | result = append(result, nodeArgs(arg.Children[0])...) 41 | } 42 | } 43 | return result 44 | } 45 | 46 | func newParseRequestFromNode(node *parser.Node) parseRequest { 47 | return parseRequest{ 48 | command: node.Value, 49 | args: nodeArgs(node), 50 | attributes: node.Attributes, 51 | original: node.Original, 52 | flags: NewBFlagsWithArgs(node.Flags), 53 | location: node.Location(), 54 | comments: node.PrevComment, 55 | } 56 | } 57 | 58 | // ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement) 59 | func ParseInstruction(node *parser.Node) (v interface{}, err error) { 60 | defer func() { 61 | err = parser.WithLocation(err, node.Location()) 62 | }() 63 | req := newParseRequestFromNode(node) 64 | switch node.Value { 65 | case command.Env: 66 | return parseEnv(req) 67 | case command.Maintainer: 68 | return parseMaintainer(req) 69 | case command.Label: 70 | return parseLabel(req) 71 | case command.Add: 72 | return parseAdd(req) 73 | case command.Copy: 74 | return parseCopy(req) 75 | case command.From: 76 | return parseFrom(req) 77 | case command.Onbuild: 78 | return parseOnBuild(req) 79 | case command.Workdir: 80 | return parseWorkdir(req) 81 | case command.Run: 82 | return parseRun(req) 83 | case command.Cmd: 84 | return parseCmd(req) 85 | case command.Healthcheck: 86 | return parseHealthcheck(req) 87 | case command.Entrypoint: 88 | return parseEntrypoint(req) 89 | case command.Expose: 90 | return parseExpose(req) 91 | case command.User: 92 | return parseUser(req) 93 | case command.Volume: 94 | return parseVolume(req) 95 | case command.StopSignal: 96 | return parseStopSignal(req) 97 | case command.Arg: 98 | return parseArg(req) 99 | case command.Shell: 100 | return parseShell(req) 101 | } 102 | 103 | return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine} 104 | } 105 | 106 | // ParseCommand converts an AST to a typed Command 107 | func ParseCommand(node *parser.Node) (Command, error) { 108 | s, err := ParseInstruction(node) 109 | if err != nil { 110 | return nil, err 111 | } 112 | if c, ok := s.(Command); ok { 113 | return c, nil 114 | } 115 | return nil, parser.WithLocation(errors.Errorf("%T is not a command type", s), node.Location()) 116 | } 117 | 118 | // UnknownInstruction represents an error occurring when a command is unresolvable 119 | type UnknownInstruction struct { 120 | Line int 121 | Instruction string 122 | } 123 | 124 | func (e *UnknownInstruction) Error() string { 125 | return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction)) 126 | } 127 | 128 | type parseError struct { 129 | inner error 130 | node *parser.Node 131 | } 132 | 133 | func (e *parseError) Error() string { 134 | return fmt.Sprintf("dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error()) 135 | } 136 | 137 | func (e *parseError) Unwrap() error { 138 | return e.inner 139 | } 140 | 141 | // Parse a Dockerfile into a collection of buildable stages. 142 | // metaArgs is a collection of ARG instructions that occur before the first FROM. 143 | func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) { 144 | for _, n := range ast.Children { 145 | cmd, err := ParseInstruction(n) 146 | if err != nil { 147 | return nil, nil, &parseError{inner: err, node: n} 148 | } 149 | if len(stages) == 0 { 150 | // meta arg case 151 | if a, isArg := cmd.(*ArgCommand); isArg { 152 | metaArgs = append(metaArgs, *a) 153 | continue 154 | } 155 | } 156 | switch c := cmd.(type) { 157 | case *Stage: 158 | stages = append(stages, *c) 159 | case Command: 160 | stage, err := CurrentStage(stages) 161 | if err != nil { 162 | return nil, nil, parser.WithLocation(err, n.Location()) 163 | } 164 | stage.AddCommand(c) 165 | default: 166 | return nil, nil, parser.WithLocation(errors.Errorf("%T is not a command type", cmd), n.Location()) 167 | } 168 | 169 | } 170 | return stages, metaArgs, nil 171 | } 172 | 173 | func parseKvps(args []string, cmdName string) (KeyValuePairs, error) { 174 | if len(args) == 0 { 175 | return nil, errAtLeastOneArgument(cmdName) 176 | } 177 | if len(args)%2 != 0 { 178 | // should never get here, but just in case 179 | return nil, errTooManyArguments(cmdName) 180 | } 181 | var res KeyValuePairs 182 | for j := 0; j < len(args); j += 2 { 183 | if len(args[j]) == 0 { 184 | return nil, errBlankCommandNames(cmdName) 185 | } 186 | name := args[j] 187 | value := args[j+1] 188 | res = append(res, KeyValuePair{Key: name, Value: value}) 189 | } 190 | return res, nil 191 | } 192 | 193 | func parseEnv(req parseRequest) (*EnvCommand, error) { 194 | 195 | if err := req.flags.Parse(); err != nil { 196 | return nil, err 197 | } 198 | envs, err := parseKvps(req.args, "ENV") 199 | if err != nil { 200 | return nil, err 201 | } 202 | return &EnvCommand{ 203 | Env: envs, 204 | withNameAndCode: newWithNameAndCode(req), 205 | }, nil 206 | } 207 | 208 | func parseMaintainer(req parseRequest) (*MaintainerCommand, error) { 209 | if len(req.args) != 1 { 210 | return nil, errExactlyOneArgument("MAINTAINER") 211 | } 212 | 213 | if err := req.flags.Parse(); err != nil { 214 | return nil, err 215 | } 216 | return &MaintainerCommand{ 217 | Maintainer: req.args[0], 218 | withNameAndCode: newWithNameAndCode(req), 219 | }, nil 220 | } 221 | 222 | func parseLabel(req parseRequest) (*LabelCommand, error) { 223 | 224 | if err := req.flags.Parse(); err != nil { 225 | return nil, err 226 | } 227 | 228 | labels, err := parseKvps(req.args, "LABEL") 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | return &LabelCommand{ 234 | Labels: labels, 235 | withNameAndCode: newWithNameAndCode(req), 236 | }, nil 237 | } 238 | 239 | func parseAdd(req parseRequest) (*AddCommand, error) { 240 | if len(req.args) < 2 { 241 | return nil, errNoDestinationArgument("ADD") 242 | } 243 | flChown := req.flags.AddString("chown", "") 244 | flChmod := req.flags.AddString("chmod", "") 245 | if err := req.flags.Parse(); err != nil { 246 | return nil, err 247 | } 248 | return &AddCommand{ 249 | SourcesAndDest: SourcesAndDest(req.args), 250 | withNameAndCode: newWithNameAndCode(req), 251 | Chown: flChown.Value, 252 | Chmod: flChmod.Value, 253 | }, nil 254 | } 255 | 256 | func parseCopy(req parseRequest) (*CopyCommand, error) { 257 | if len(req.args) < 2 { 258 | return nil, errNoDestinationArgument("COPY") 259 | } 260 | flChown := req.flags.AddString("chown", "") 261 | flFrom := req.flags.AddString("from", "") 262 | flChmod := req.flags.AddString("chmod", "") 263 | if err := req.flags.Parse(); err != nil { 264 | return nil, err 265 | } 266 | return &CopyCommand{ 267 | SourcesAndDest: SourcesAndDest(req.args), 268 | From: flFrom.Value, 269 | withNameAndCode: newWithNameAndCode(req), 270 | Chown: flChown.Value, 271 | Chmod: flChmod.Value, 272 | }, nil 273 | } 274 | 275 | func parseFrom(req parseRequest) (*Stage, error) { 276 | stageName, err := parseBuildStageName(req.args) 277 | if err != nil { 278 | return nil, err 279 | } 280 | 281 | flPlatform := req.flags.AddString("platform", "") 282 | if err := req.flags.Parse(); err != nil { 283 | return nil, err 284 | } 285 | 286 | code := strings.TrimSpace(req.original) 287 | return &Stage{ 288 | BaseName: req.args[0], 289 | Name: stageName, 290 | SourceCode: code, 291 | Commands: []Command{}, 292 | Platform: flPlatform.Value, 293 | Location: req.location, 294 | Comment: getComment(req.comments, stageName), 295 | }, nil 296 | 297 | } 298 | 299 | func parseBuildStageName(args []string) (string, error) { 300 | stageName := "" 301 | switch { 302 | case len(args) == 3 && strings.EqualFold(args[1], "as"): 303 | stageName = strings.ToLower(args[2]) 304 | if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok { 305 | return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", args[2]) 306 | } 307 | case len(args) != 1: 308 | return "", errors.New("FROM requires either one or three arguments") 309 | } 310 | 311 | return stageName, nil 312 | } 313 | 314 | func parseOnBuild(req parseRequest) (*OnbuildCommand, error) { 315 | if len(req.args) == 0 { 316 | return nil, errAtLeastOneArgument("ONBUILD") 317 | } 318 | if err := req.flags.Parse(); err != nil { 319 | return nil, err 320 | } 321 | 322 | triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0])) 323 | switch strings.ToUpper(triggerInstruction) { 324 | case "ONBUILD": 325 | return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") 326 | case "MAINTAINER", "FROM": 327 | return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) 328 | } 329 | 330 | original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") 331 | return &OnbuildCommand{ 332 | Expression: original, 333 | withNameAndCode: newWithNameAndCode(req), 334 | }, nil 335 | 336 | } 337 | 338 | func parseWorkdir(req parseRequest) (*WorkdirCommand, error) { 339 | if len(req.args) != 1 { 340 | return nil, errExactlyOneArgument("WORKDIR") 341 | } 342 | 343 | err := req.flags.Parse() 344 | if err != nil { 345 | return nil, err 346 | } 347 | return &WorkdirCommand{ 348 | Path: req.args[0], 349 | withNameAndCode: newWithNameAndCode(req), 350 | }, nil 351 | 352 | } 353 | 354 | func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine { 355 | args := handleJSONArgs(req.args, req.attributes) 356 | cmd := strslice.StrSlice(args) 357 | if emptyAsNil && len(cmd) == 0 { 358 | cmd = nil 359 | } 360 | return ShellDependantCmdLine{ 361 | CmdLine: cmd, 362 | PrependShell: !req.attributes["json"], 363 | } 364 | } 365 | 366 | func parseRun(req parseRequest) (*RunCommand, error) { 367 | cmd := &RunCommand{} 368 | 369 | for _, fn := range parseRunPreHooks { 370 | if err := fn(cmd, req); err != nil { 371 | return nil, err 372 | } 373 | } 374 | 375 | if err := req.flags.Parse(); err != nil { 376 | return nil, err 377 | } 378 | 379 | cmd.ShellDependantCmdLine = parseShellDependentCommand(req, false) 380 | cmd.withNameAndCode = newWithNameAndCode(req) 381 | 382 | for _, fn := range parseRunPostHooks { 383 | if err := fn(cmd, req); err != nil { 384 | return nil, err 385 | } 386 | } 387 | 388 | return cmd, nil 389 | } 390 | 391 | func parseCmd(req parseRequest) (*CmdCommand, error) { 392 | if err := req.flags.Parse(); err != nil { 393 | return nil, err 394 | } 395 | return &CmdCommand{ 396 | ShellDependantCmdLine: parseShellDependentCommand(req, false), 397 | withNameAndCode: newWithNameAndCode(req), 398 | }, nil 399 | 400 | } 401 | 402 | func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) { 403 | if err := req.flags.Parse(); err != nil { 404 | return nil, err 405 | } 406 | 407 | cmd := &EntrypointCommand{ 408 | ShellDependantCmdLine: parseShellDependentCommand(req, true), 409 | withNameAndCode: newWithNameAndCode(req), 410 | } 411 | 412 | return cmd, nil 413 | } 414 | 415 | // parseOptInterval(flag) is the duration of flag.Value, or 0 if 416 | // empty. An error is reported if the value is given and less than minimum duration. 417 | func parseOptInterval(f *Flag) (time.Duration, error) { 418 | s := f.Value 419 | if s == "" { 420 | return 0, nil 421 | } 422 | d, err := time.ParseDuration(s) 423 | if err != nil { 424 | return 0, err 425 | } 426 | if d < container.MinimumDuration { 427 | return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration) 428 | } 429 | return d, nil 430 | } 431 | func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) { 432 | if len(req.args) == 0 { 433 | return nil, errAtLeastOneArgument("HEALTHCHECK") 434 | } 435 | cmd := &HealthCheckCommand{ 436 | withNameAndCode: newWithNameAndCode(req), 437 | } 438 | 439 | typ := strings.ToUpper(req.args[0]) 440 | args := req.args[1:] 441 | if typ == "NONE" { 442 | if len(args) != 0 { 443 | return nil, errors.New("HEALTHCHECK NONE takes no arguments") 444 | } 445 | test := strslice.StrSlice{typ} 446 | cmd.Health = &container.HealthConfig{ 447 | Test: test, 448 | } 449 | } else { 450 | 451 | healthcheck := container.HealthConfig{} 452 | 453 | flInterval := req.flags.AddString("interval", "") 454 | flTimeout := req.flags.AddString("timeout", "") 455 | flStartPeriod := req.flags.AddString("start-period", "") 456 | flRetries := req.flags.AddString("retries", "") 457 | 458 | if err := req.flags.Parse(); err != nil { 459 | return nil, err 460 | } 461 | 462 | switch typ { 463 | case "CMD": 464 | cmdSlice := handleJSONArgs(args, req.attributes) 465 | if len(cmdSlice) == 0 { 466 | return nil, errors.New("Missing command after HEALTHCHECK CMD") 467 | } 468 | 469 | if !req.attributes["json"] { 470 | typ = "CMD-SHELL" 471 | } 472 | 473 | healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...)) 474 | default: 475 | return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ) 476 | } 477 | 478 | interval, err := parseOptInterval(flInterval) 479 | if err != nil { 480 | return nil, err 481 | } 482 | healthcheck.Interval = interval 483 | 484 | timeout, err := parseOptInterval(flTimeout) 485 | if err != nil { 486 | return nil, err 487 | } 488 | healthcheck.Timeout = timeout 489 | 490 | startPeriod, err := parseOptInterval(flStartPeriod) 491 | if err != nil { 492 | return nil, err 493 | } 494 | healthcheck.StartPeriod = startPeriod 495 | 496 | if flRetries.Value != "" { 497 | retries, err := strconv.ParseInt(flRetries.Value, 10, 32) 498 | if err != nil { 499 | return nil, err 500 | } 501 | if retries < 1 { 502 | return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries) 503 | } 504 | healthcheck.Retries = int(retries) 505 | } else { 506 | healthcheck.Retries = 0 507 | } 508 | 509 | cmd.Health = &healthcheck 510 | } 511 | return cmd, nil 512 | } 513 | 514 | func parseExpose(req parseRequest) (*ExposeCommand, error) { 515 | portsTab := req.args 516 | 517 | if len(req.args) == 0 { 518 | return nil, errAtLeastOneArgument("EXPOSE") 519 | } 520 | 521 | if err := req.flags.Parse(); err != nil { 522 | return nil, err 523 | } 524 | 525 | sort.Strings(portsTab) 526 | return &ExposeCommand{ 527 | Ports: portsTab, 528 | withNameAndCode: newWithNameAndCode(req), 529 | }, nil 530 | } 531 | 532 | func parseUser(req parseRequest) (*UserCommand, error) { 533 | if len(req.args) != 1 { 534 | return nil, errExactlyOneArgument("USER") 535 | } 536 | 537 | if err := req.flags.Parse(); err != nil { 538 | return nil, err 539 | } 540 | return &UserCommand{ 541 | User: req.args[0], 542 | withNameAndCode: newWithNameAndCode(req), 543 | }, nil 544 | } 545 | 546 | func parseVolume(req parseRequest) (*VolumeCommand, error) { 547 | if len(req.args) == 0 { 548 | return nil, errAtLeastOneArgument("VOLUME") 549 | } 550 | 551 | if err := req.flags.Parse(); err != nil { 552 | return nil, err 553 | } 554 | 555 | cmd := &VolumeCommand{ 556 | withNameAndCode: newWithNameAndCode(req), 557 | } 558 | 559 | for _, v := range req.args { 560 | v = strings.TrimSpace(v) 561 | if v == "" { 562 | return nil, errors.New("VOLUME specified can not be an empty string") 563 | } 564 | cmd.Volumes = append(cmd.Volumes, v) 565 | } 566 | return cmd, nil 567 | 568 | } 569 | 570 | func parseStopSignal(req parseRequest) (*StopSignalCommand, error) { 571 | if len(req.args) != 1 { 572 | return nil, errExactlyOneArgument("STOPSIGNAL") 573 | } 574 | sig := req.args[0] 575 | 576 | cmd := &StopSignalCommand{ 577 | Signal: sig, 578 | withNameAndCode: newWithNameAndCode(req), 579 | } 580 | return cmd, nil 581 | 582 | } 583 | 584 | func parseArg(req parseRequest) (*ArgCommand, error) { 585 | if len(req.args) < 1 { 586 | return nil, errAtLeastOneArgument("ARG") 587 | } 588 | 589 | pairs := make([]KeyValuePairOptional, len(req.args)) 590 | 591 | for i, arg := range req.args { 592 | kvpo := KeyValuePairOptional{} 593 | 594 | // 'arg' can just be a name or name-value pair. Note that this is different 595 | // from 'env' that handles the split of name and value at the parser level. 596 | // The reason for doing it differently for 'arg' is that we support just 597 | // defining an arg and not assign it a value (while 'env' always expects a 598 | // name-value pair). If possible, it will be good to harmonize the two. 599 | if strings.Contains(arg, "=") { 600 | parts := strings.SplitN(arg, "=", 2) 601 | if len(parts[0]) == 0 { 602 | return nil, errBlankCommandNames("ARG") 603 | } 604 | 605 | kvpo.Key = parts[0] 606 | kvpo.Value = &parts[1] 607 | } else { 608 | kvpo.Key = arg 609 | } 610 | kvpo.Comment = getComment(req.comments, kvpo.Key) 611 | pairs[i] = kvpo 612 | } 613 | 614 | return &ArgCommand{ 615 | Args: pairs, 616 | withNameAndCode: newWithNameAndCode(req), 617 | }, nil 618 | } 619 | 620 | func parseShell(req parseRequest) (*ShellCommand, error) { 621 | if err := req.flags.Parse(); err != nil { 622 | return nil, err 623 | } 624 | shellSlice := handleJSONArgs(req.args, req.attributes) 625 | switch { 626 | case len(shellSlice) == 0: 627 | // SHELL [] 628 | return nil, errAtLeastOneArgument("SHELL") 629 | case req.attributes["json"]: 630 | // SHELL ["powershell", "-command"] 631 | 632 | return &ShellCommand{ 633 | Shell: strslice.StrSlice(shellSlice), 634 | withNameAndCode: newWithNameAndCode(req), 635 | }, nil 636 | default: 637 | // SHELL powershell -command - not JSON 638 | return nil, errNotJSON("SHELL", req.original) 639 | } 640 | } 641 | 642 | func errAtLeastOneArgument(command string) error { 643 | return errors.Errorf("%s requires at least one argument", command) 644 | } 645 | 646 | func errExactlyOneArgument(command string) error { 647 | return errors.Errorf("%s requires exactly one argument", command) 648 | } 649 | 650 | func errNoDestinationArgument(command string) error { 651 | return errors.Errorf("%s requires at least two arguments, but only one was provided. Destination could not be determined.", command) 652 | } 653 | 654 | func errBlankCommandNames(command string) error { 655 | return errors.Errorf("%s names can not be blank", command) 656 | } 657 | 658 | func errTooManyArguments(command string) error { 659 | return errors.Errorf("Bad input to %s, too many arguments", command) 660 | } 661 | 662 | func getComment(comments []string, name string) string { 663 | if name == "" { 664 | return "" 665 | } 666 | for _, line := range comments { 667 | if strings.HasPrefix(line, name+" ") { 668 | return strings.TrimPrefix(line, name+" ") 669 | } 670 | } 671 | return "" 672 | } 673 | -------------------------------------------------------------------------------- /ickfile/instructions/parse_test.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/agbell/compiling-containers/ickfile/command" 9 | "github.com/agbell/compiling-containers/ickfile/parser" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestCommandsExactlyOneArgument(t *testing.T) { 14 | commands := []string{ 15 | "MAINTAINER", 16 | "WORKDIR", 17 | "USER", 18 | "STOPSIGNAL", 19 | } 20 | 21 | for _, cmd := range commands { 22 | ast, err := parser.Parse(strings.NewReader(cmd)) 23 | require.NoError(t, err) 24 | _, err = ParseInstruction(ast.AST.Children[0]) 25 | require.EqualError(t, err, errExactlyOneArgument(cmd).Error()) 26 | } 27 | } 28 | 29 | func TestCommandsAtLeastOneArgument(t *testing.T) { 30 | commands := []string{ 31 | "ENV", 32 | "LABEL", 33 | "ONBUILD", 34 | "HEALTHCHECK", 35 | "EXPOSE", 36 | "VOLUME", 37 | } 38 | 39 | for _, cmd := range commands { 40 | ast, err := parser.Parse(strings.NewReader(cmd)) 41 | require.NoError(t, err) 42 | _, err = ParseInstruction(ast.AST.Children[0]) 43 | require.EqualError(t, err, errAtLeastOneArgument(cmd).Error()) 44 | } 45 | } 46 | 47 | func TestCommandsNoDestinationArgument(t *testing.T) { 48 | commands := []string{ 49 | "ADD", 50 | "COPY", 51 | } 52 | 53 | for _, cmd := range commands { 54 | ast, err := parser.Parse(strings.NewReader(cmd + " arg1")) 55 | require.NoError(t, err) 56 | _, err = ParseInstruction(ast.AST.Children[0]) 57 | require.EqualError(t, err, errNoDestinationArgument(cmd).Error()) 58 | } 59 | } 60 | 61 | func TestCommandsTooManyArguments(t *testing.T) { 62 | commands := []string{ 63 | "ENV", 64 | "LABEL", 65 | } 66 | 67 | for _, command := range commands { 68 | node := &parser.Node{ 69 | Original: command + "arg1 arg2 arg3", 70 | Value: strings.ToLower(command), 71 | Next: &parser.Node{ 72 | Value: "arg1", 73 | Next: &parser.Node{ 74 | Value: "arg2", 75 | Next: &parser.Node{ 76 | Value: "arg3", 77 | }, 78 | }, 79 | }, 80 | } 81 | _, err := ParseInstruction(node) 82 | require.EqualError(t, err, errTooManyArguments(command).Error()) 83 | } 84 | } 85 | 86 | func TestCommandsBlankNames(t *testing.T) { 87 | commands := []string{ 88 | "ENV", 89 | "LABEL", 90 | } 91 | 92 | for _, cmd := range commands { 93 | node := &parser.Node{ 94 | Original: cmd + " =arg2", 95 | Value: strings.ToLower(cmd), 96 | Next: &parser.Node{ 97 | Value: "", 98 | Next: &parser.Node{ 99 | Value: "arg2", 100 | }, 101 | }, 102 | } 103 | _, err := ParseInstruction(node) 104 | require.EqualError(t, err, errBlankCommandNames(cmd).Error()) 105 | } 106 | } 107 | 108 | func TestHealthCheckCmd(t *testing.T) { 109 | node := &parser.Node{ 110 | Value: command.Healthcheck, 111 | Next: &parser.Node{ 112 | Value: "CMD", 113 | Next: &parser.Node{ 114 | Value: "hello", 115 | Next: &parser.Node{ 116 | Value: "world", 117 | }, 118 | }, 119 | }, 120 | } 121 | cmd, err := ParseInstruction(node) 122 | require.NoError(t, err) 123 | hc, ok := cmd.(*HealthCheckCommand) 124 | require.Equal(t, true, ok) 125 | expected := []string{"CMD-SHELL", "hello world"} 126 | require.Equal(t, expected, hc.Health.Test) 127 | } 128 | 129 | func TestParseOptInterval(t *testing.T) { 130 | flInterval := &Flag{ 131 | name: "interval", 132 | flagType: stringType, 133 | Value: "50ns", 134 | } 135 | _, err := parseOptInterval(flInterval) 136 | require.Error(t, err) 137 | require.Contains(t, err.Error(), "cannot be less than 1ms") 138 | 139 | flInterval.Value = "1ms" 140 | _, err = parseOptInterval(flInterval) 141 | require.NoError(t, err) 142 | } 143 | 144 | func TestCommentsDetection(t *testing.T) { 145 | dt := `# foo sets foo 146 | ARG foo=bar 147 | 148 | # base defines first stage 149 | FROM busybox AS base 150 | # this is irrelevant 151 | ARG foo 152 | # bar defines bar 153 | # baz is something else 154 | ARG bar baz=123 155 | ` 156 | 157 | ast, err := parser.Parse(bytes.NewBuffer([]byte(dt))) 158 | require.NoError(t, err) 159 | 160 | stages, meta, err := Parse(ast.AST) 161 | require.NoError(t, err) 162 | 163 | require.Equal(t, "defines first stage", stages[0].Comment) 164 | require.Equal(t, "foo", meta[0].Args[0].Key) 165 | require.Equal(t, "sets foo", meta[0].Args[0].Comment) 166 | 167 | st := stages[0] 168 | 169 | require.Equal(t, "foo", st.Commands[0].(*ArgCommand).Args[0].Key) 170 | require.Equal(t, "", st.Commands[0].(*ArgCommand).Args[0].Comment) 171 | require.Equal(t, "bar", st.Commands[1].(*ArgCommand).Args[0].Key) 172 | require.Equal(t, "defines bar", st.Commands[1].(*ArgCommand).Args[0].Comment) 173 | require.Equal(t, "baz", st.Commands[1].(*ArgCommand).Args[1].Key) 174 | require.Equal(t, "is something else", st.Commands[1].(*ArgCommand).Args[1].Comment) 175 | } 176 | 177 | func TestErrorCases(t *testing.T) { 178 | cases := []struct { 179 | name string 180 | dockerfile string 181 | expectedError string 182 | }{ 183 | { 184 | name: "copyEmptyWhitespace", 185 | dockerfile: `COPY 186 | quux \ 187 | bar`, 188 | expectedError: "COPY requires at least two arguments", 189 | }, 190 | { 191 | name: "ONBUILD forbidden FROM", 192 | dockerfile: "ONBUILD FROM scratch", 193 | expectedError: "FROM isn't allowed as an ONBUILD trigger", 194 | }, 195 | { 196 | name: "ONBUILD forbidden MAINTAINER", 197 | dockerfile: "ONBUILD MAINTAINER docker.io", 198 | expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger", 199 | }, 200 | { 201 | name: "MAINTAINER unknown flag", 202 | dockerfile: "MAINTAINER --boo joe@example.com", 203 | expectedError: "Unknown flag: boo", 204 | }, 205 | { 206 | name: "Chaining ONBUILD", 207 | dockerfile: `ONBUILD ONBUILD RUN touch foobar`, 208 | expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed", 209 | }, 210 | { 211 | name: "Invalid instruction", 212 | dockerfile: `foo bar`, 213 | expectedError: "unknown instruction: FOO", 214 | }, 215 | } 216 | for _, c := range cases { 217 | r := strings.NewReader(c.dockerfile) 218 | ast, err := parser.Parse(r) 219 | 220 | if err != nil { 221 | t.Fatalf("Error when parsing Dockerfile: %s", err) 222 | } 223 | n := ast.AST.Children[0] 224 | _, err = ParseInstruction(n) 225 | require.Error(t, err) 226 | require.Contains(t, err.Error(), c.expectedError) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /ickfile/instructions/support.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import "strings" 4 | 5 | // handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile 6 | // for exec form it returns untouched args slice 7 | // for shell form it returns concatenated args as the first element of a slice 8 | func handleJSONArgs(args []string, attributes map[string]bool) []string { 9 | if len(args) == 0 { 10 | return []string{} 11 | } 12 | 13 | if attributes != nil && attributes["json"] { 14 | return args 15 | } 16 | 17 | // literal string command, not an exec array 18 | return []string{strings.Join(args, " ")} 19 | } 20 | -------------------------------------------------------------------------------- /ickfile/instructions/support_test.go: -------------------------------------------------------------------------------- 1 | package instructions 2 | 3 | import "testing" 4 | 5 | type testCase struct { 6 | name string 7 | args []string 8 | attributes map[string]bool 9 | expected []string 10 | } 11 | 12 | func initTestCases() []testCase { 13 | var testCases []testCase 14 | 15 | testCases = append(testCases, testCase{ 16 | name: "empty args", 17 | args: []string{}, 18 | attributes: make(map[string]bool), 19 | expected: []string{}, 20 | }) 21 | 22 | jsonAttributes := make(map[string]bool) 23 | jsonAttributes["json"] = true 24 | 25 | testCases = append(testCases, testCase{ 26 | name: "json attribute with one element", 27 | args: []string{"foo"}, 28 | attributes: jsonAttributes, 29 | expected: []string{"foo"}, 30 | }) 31 | 32 | testCases = append(testCases, testCase{ 33 | name: "json attribute with two elements", 34 | args: []string{"foo", "bar"}, 35 | attributes: jsonAttributes, 36 | expected: []string{"foo", "bar"}, 37 | }) 38 | 39 | testCases = append(testCases, testCase{ 40 | name: "no attributes", 41 | args: []string{"foo", "bar"}, 42 | attributes: nil, 43 | expected: []string{"foo bar"}, 44 | }) 45 | 46 | return testCases 47 | } 48 | 49 | func TestHandleJSONArgs(t *testing.T) { 50 | testCases := initTestCases() 51 | 52 | for _, test := range testCases { 53 | arguments := handleJSONArgs(test.args, test.attributes) 54 | 55 | if len(arguments) != len(test.expected) { 56 | t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments)) 57 | } 58 | 59 | for i := range test.expected { 60 | if arguments[i] != test.expected[i] { 61 | t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i]) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ickfile/parser/dumper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/agbell/compiling-containers/ickfile/parser" 8 | ) 9 | 10 | func main() { 11 | var f *os.File 12 | var err error 13 | 14 | if len(os.Args) < 2 { 15 | fmt.Println("please supply filename(s)") 16 | os.Exit(1) 17 | } 18 | 19 | for _, fn := range os.Args[1:] { 20 | f, err = os.Open(fn) 21 | if err != nil { 22 | panic(err) 23 | } 24 | defer f.Close() 25 | 26 | result, err := parser.Parse(f) 27 | if err != nil { 28 | panic(err) 29 | } 30 | fmt.Println(result.AST.Dump()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ickfile/parser/errors.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/moby/buildkit/util/stack" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // ErrorLocation gives a location in source code that caused the error 9 | type ErrorLocation struct { 10 | Location []Range 11 | error 12 | } 13 | 14 | // Unwrap unwraps to the next error 15 | func (e *ErrorLocation) Unwrap() error { 16 | return e.error 17 | } 18 | 19 | // Range is a code section between two positions 20 | type Range struct { 21 | Start Position 22 | End Position 23 | } 24 | 25 | // Position is a point in source code 26 | type Position struct { 27 | Line int 28 | Character int 29 | } 30 | 31 | func withLocation(err error, start, end int) error { 32 | return WithLocation(err, toRanges(start, end)) 33 | } 34 | 35 | // WithLocation extends an error with a source code location 36 | func WithLocation(err error, location []Range) error { 37 | if err == nil { 38 | return nil 39 | } 40 | var el *ErrorLocation 41 | if errors.As(err, &el) { 42 | return err 43 | } 44 | return stack.Enable(&ErrorLocation{ 45 | error: err, 46 | Location: location, 47 | }) 48 | } 49 | 50 | func toRanges(start, end int) (r []Range) { 51 | if end <= start { 52 | end = start 53 | } 54 | for i := start; i <= end; i++ { 55 | r = append(r, Range{Start: Position{Line: i}, End: Position{Line: i}}) 56 | } 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /ickfile/parser/json_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var invalidJSONArraysOfStrings = []string{ 8 | `["a",42,"b"]`, 9 | `["a",123.456,"b"]`, 10 | `["a",{},"b"]`, 11 | `["a",{"c": "d"},"b"]`, 12 | `["a",["c"],"b"]`, 13 | `["a",true,"b"]`, 14 | `["a",false,"b"]`, 15 | `["a",null,"b"]`, 16 | } 17 | 18 | var validJSONArraysOfStrings = map[string][]string{ 19 | `[]`: {}, 20 | `[""]`: {""}, 21 | `["a"]`: {"a"}, 22 | `["a","b"]`: {"a", "b"}, 23 | `[ "a", "b" ]`: {"a", "b"}, 24 | `[ "a", "b" ]`: {"a", "b"}, 25 | ` [ "a", "b" ] `: {"a", "b"}, 26 | `["abc 123", "♥", "☃", "\" \\ \/ \b \f \n \r \t \u0000"]`: {"abc 123", "♥", "☃", "\" \\ / \b \f \n \r \t \u0000"}, 27 | } 28 | 29 | func TestJSONArraysOfStrings(t *testing.T) { 30 | for json, expected := range validJSONArraysOfStrings { 31 | d := newDefaultDirectives() 32 | 33 | if node, _, err := parseJSON(json, d); err != nil { 34 | t.Fatalf("%q should be a valid JSON array of strings, but wasn't! (err: %q)", json, err) 35 | } else { 36 | i := 0 37 | for node != nil { 38 | if i >= len(expected) { 39 | t.Fatalf("expected result is shorter than parsed result (%d vs %d+) in %q", len(expected), i+1, json) 40 | } 41 | if node.Value != expected[i] { 42 | t.Fatalf("expected %q (not %q) in %q at pos %d", expected[i], node.Value, json, i) 43 | } 44 | node = node.Next 45 | i++ 46 | } 47 | if i != len(expected) { 48 | t.Fatalf("expected result is longer than parsed result (%d vs %d) in %q", len(expected), i+1, json) 49 | } 50 | } 51 | } 52 | for _, json := range invalidJSONArraysOfStrings { 53 | d := newDefaultDirectives() 54 | 55 | if _, _, err := parseJSON(json, d); err != errDockerfileNotStringArray { 56 | t.Fatalf("%q should be an invalid JSON array of strings, but wasn't!", json) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ickfile/parser/line_parsers.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // line parsers are dispatch calls that parse a single unit of text into a 4 | // Node object which contains the whole statement. Dockerfiles have varied 5 | // (but not usually unique, see ONBUILD for a unique example) parsing rules 6 | // per-command, and these unify the processing in a way that makes it 7 | // manageable. 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "strings" 13 | "unicode" 14 | "unicode/utf8" 15 | 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | var ( 20 | errDockerfileNotStringArray = errors.New("when using JSON array syntax, arrays must be comprised of strings only") 21 | ) 22 | 23 | const ( 24 | commandLabel = "LABEL" 25 | ) 26 | 27 | // ignore the current argument. This will still leave a command parsed, but 28 | // will not incorporate the arguments into the ast. 29 | func parseIgnore(rest string, d *directives) (*Node, map[string]bool, error) { 30 | return &Node{}, nil, nil 31 | } 32 | 33 | // used for onbuild. Could potentially be used for anything that represents a 34 | // statement with sub-statements. 35 | // 36 | // ONBUILD RUN foo bar -> (onbuild (please foo bar)) 37 | // 38 | func parseSubCommand(rest string, d *directives) (*Node, map[string]bool, error) { 39 | if rest == "" { 40 | return nil, nil, nil 41 | } 42 | 43 | child, err := newNodeFromLine(rest, d, nil) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | 48 | return &Node{Children: []*Node{child}}, nil, nil 49 | } 50 | 51 | // helper to parse words (i.e space delimited or quoted strings) in a statement. 52 | // The quotes are preserved as part of this function and they are stripped later 53 | // as part of processWords(). 54 | func parseWords(rest string, d *directives) []string { 55 | const ( 56 | inSpaces = iota // looking for start of a word 57 | inWord 58 | inQuote 59 | ) 60 | 61 | words := []string{} 62 | phase := inSpaces 63 | word := "" 64 | quote := '\000' 65 | blankOK := false 66 | var ch rune 67 | var chWidth int 68 | 69 | for pos := 0; pos <= len(rest); pos += chWidth { 70 | if pos != len(rest) { 71 | ch, chWidth = utf8.DecodeRuneInString(rest[pos:]) 72 | } 73 | 74 | if phase == inSpaces { // Looking for start of word 75 | if pos == len(rest) { // end of input 76 | break 77 | } 78 | if unicode.IsSpace(ch) { // skip spaces 79 | continue 80 | } 81 | phase = inWord // found it, fall through 82 | } 83 | if (phase == inWord || phase == inQuote) && (pos == len(rest)) { 84 | if blankOK || len(word) > 0 { 85 | words = append(words, word) 86 | } 87 | break 88 | } 89 | if phase == inWord { 90 | if unicode.IsSpace(ch) { 91 | phase = inSpaces 92 | if blankOK || len(word) > 0 { 93 | words = append(words, word) 94 | } 95 | word = "" 96 | blankOK = false 97 | continue 98 | } 99 | if ch == '\'' || ch == '"' { 100 | quote = ch 101 | blankOK = true 102 | phase = inQuote 103 | } 104 | if ch == d.escapeToken { 105 | if pos+chWidth == len(rest) { 106 | continue // just skip an escape token at end of line 107 | } 108 | // If we're not quoted and we see an escape token, then always just 109 | // add the escape token plus the char to the word, even if the char 110 | // is a quote. 111 | word += string(ch) 112 | pos += chWidth 113 | ch, chWidth = utf8.DecodeRuneInString(rest[pos:]) 114 | } 115 | word += string(ch) 116 | continue 117 | } 118 | if phase == inQuote { 119 | if ch == quote { 120 | phase = inWord 121 | } 122 | // The escape token is special except for ' quotes - can't escape anything for ' 123 | if ch == d.escapeToken && quote != '\'' { 124 | if pos+chWidth == len(rest) { 125 | phase = inWord 126 | continue // just skip the escape token at end 127 | } 128 | pos += chWidth 129 | word += string(ch) 130 | ch, chWidth = utf8.DecodeRuneInString(rest[pos:]) 131 | } 132 | word += string(ch) 133 | } 134 | } 135 | 136 | return words 137 | } 138 | 139 | // parse environment like statements. Note that this does *not* handle 140 | // variable interpolation, which will be handled in the evaluator. 141 | func parseNameVal(rest string, key string, d *directives) (*Node, error) { 142 | // This is kind of tricky because we need to support the old 143 | // variant: KEY name value 144 | // as well as the new one: KEY name=value ... 145 | // The trigger to know which one is being used will be whether we hit 146 | // a space or = first. space ==> old, "=" ==> new 147 | 148 | words := parseWords(rest, d) 149 | if len(words) == 0 { 150 | return nil, nil 151 | } 152 | 153 | // Old format (KEY name value) 154 | if !strings.Contains(words[0], "=") { 155 | parts := reWhitespace.Split(rest, 2) 156 | if len(parts) < 2 { 157 | return nil, fmt.Errorf(key + " must have two arguments") 158 | } 159 | return newKeyValueNode(parts[0], parts[1]), nil 160 | } 161 | 162 | var rootNode *Node 163 | var prevNode *Node 164 | for _, word := range words { 165 | if !strings.Contains(word, "=") { 166 | return nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word) 167 | } 168 | 169 | parts := strings.SplitN(word, "=", 2) 170 | node := newKeyValueNode(parts[0], parts[1]) 171 | rootNode, prevNode = appendKeyValueNode(node, rootNode, prevNode) 172 | } 173 | 174 | return rootNode, nil 175 | } 176 | 177 | func newKeyValueNode(key, value string) *Node { 178 | return &Node{ 179 | Value: key, 180 | Next: &Node{Value: value}, 181 | } 182 | } 183 | 184 | func appendKeyValueNode(node, rootNode, prevNode *Node) (*Node, *Node) { 185 | if rootNode == nil { 186 | rootNode = node 187 | } 188 | if prevNode != nil { 189 | prevNode.Next = node 190 | } 191 | 192 | prevNode = node.Next 193 | return rootNode, prevNode 194 | } 195 | 196 | func parseEnv(rest string, d *directives) (*Node, map[string]bool, error) { 197 | node, err := parseNameVal(rest, "ENV", d) 198 | return node, nil, err 199 | } 200 | 201 | func parseLabel(rest string, d *directives) (*Node, map[string]bool, error) { 202 | node, err := parseNameVal(rest, commandLabel, d) 203 | return node, nil, err 204 | } 205 | 206 | // parses a statement containing one or more keyword definition(s) and/or 207 | // value assignments, like `name1 name2= name3="" name4=value`. 208 | // Note that this is a stricter format than the old format of assignment, 209 | // allowed by parseNameVal(), in a way that this only allows assignment of the 210 | // form `keyword=[]` like `name2=`, `name3=""`, and `name4=value` above. 211 | // In addition, a keyword definition alone is of the form `keyword` like `name1` 212 | // above. And the assignments `name2=` and `name3=""` are equivalent and 213 | // assign an empty value to the respective keywords. 214 | func parseNameOrNameVal(rest string, d *directives) (*Node, map[string]bool, error) { 215 | words := parseWords(rest, d) 216 | if len(words) == 0 { 217 | return nil, nil, nil 218 | } 219 | 220 | var ( 221 | rootnode *Node 222 | prevNode *Node 223 | ) 224 | for i, word := range words { 225 | node := &Node{} 226 | node.Value = word 227 | if i == 0 { 228 | rootnode = node 229 | } else { 230 | prevNode.Next = node 231 | } 232 | prevNode = node 233 | } 234 | 235 | return rootnode, nil, nil 236 | } 237 | 238 | // parses a whitespace-delimited set of arguments. The result is effectively a 239 | // linked list of string arguments. 240 | func parseStringsWhitespaceDelimited(rest string, d *directives) (*Node, map[string]bool, error) { 241 | if rest == "" { 242 | return nil, nil, nil 243 | } 244 | 245 | node := &Node{} 246 | rootnode := node 247 | prevnode := node 248 | for _, str := range reWhitespace.Split(rest, -1) { // use regexp 249 | prevnode = node 250 | node.Value = str 251 | node.Next = &Node{} 252 | node = node.Next 253 | } 254 | 255 | // XXX to get around regexp.Split *always* providing an empty string at the 256 | // end due to how our loop is constructed, nil out the last node in the 257 | // chain. 258 | prevnode.Next = nil 259 | 260 | return rootnode, nil, nil 261 | } 262 | 263 | // parseString just wraps the string in quotes and returns a working node. 264 | func parseString(rest string, d *directives) (*Node, map[string]bool, error) { 265 | if rest == "" { 266 | return nil, nil, nil 267 | } 268 | n := &Node{} 269 | n.Value = rest 270 | return n, nil, nil 271 | } 272 | 273 | // parseJSON converts JSON arrays to an AST. 274 | func parseJSON(rest string, d *directives) (*Node, map[string]bool, error) { 275 | rest = strings.TrimLeftFunc(rest, unicode.IsSpace) 276 | if !strings.HasPrefix(rest, "[") { 277 | return nil, nil, fmt.Errorf(`Error parsing "%s" as a JSON array`, rest) 278 | } 279 | 280 | var myJSON []interface{} 281 | if err := json.NewDecoder(strings.NewReader(rest)).Decode(&myJSON); err != nil { 282 | return nil, nil, err 283 | } 284 | 285 | var top, prev *Node 286 | for _, str := range myJSON { 287 | s, ok := str.(string) 288 | if !ok { 289 | return nil, nil, errDockerfileNotStringArray 290 | } 291 | 292 | node := &Node{Value: s} 293 | if prev == nil { 294 | top = node 295 | } else { 296 | prev.Next = node 297 | } 298 | prev = node 299 | } 300 | 301 | return top, map[string]bool{"json": true}, nil 302 | } 303 | 304 | // parseMaybeJSON determines if the argument appears to be a JSON array. If 305 | // so, passes to parseJSON; if not, quotes the result and returns a single 306 | // node. 307 | func parseMaybeJSON(rest string, d *directives) (*Node, map[string]bool, error) { 308 | if rest == "" { 309 | return nil, nil, nil 310 | } 311 | 312 | node, attrs, err := parseJSON(rest, d) 313 | 314 | if err == nil { 315 | return node, attrs, nil 316 | } 317 | if err == errDockerfileNotStringArray { 318 | return nil, nil, err 319 | } 320 | 321 | node = &Node{} 322 | node.Value = rest 323 | return node, nil, nil 324 | } 325 | 326 | // parseMaybeJSONToList determines if the argument appears to be a JSON array. If 327 | // so, passes to parseJSON; if not, attempts to parse it as a whitespace 328 | // delimited string. 329 | func parseMaybeJSONToList(rest string, d *directives) (*Node, map[string]bool, error) { 330 | node, attrs, err := parseJSON(rest, d) 331 | 332 | if err == nil { 333 | return node, attrs, nil 334 | } 335 | if err == errDockerfileNotStringArray { 336 | return nil, nil, err 337 | } 338 | 339 | return parseStringsWhitespaceDelimited(rest, d) 340 | } 341 | 342 | // The HEALTHCHECK command is like parseMaybeJSON, but has an extra type argument. 343 | func parseHealthConfig(rest string, d *directives) (*Node, map[string]bool, error) { 344 | // Find end of first argument 345 | var sep int 346 | for ; sep < len(rest); sep++ { 347 | if unicode.IsSpace(rune(rest[sep])) { 348 | break 349 | } 350 | } 351 | next := sep 352 | for ; next < len(rest); next++ { 353 | if !unicode.IsSpace(rune(rest[next])) { 354 | break 355 | } 356 | } 357 | 358 | if sep == 0 { 359 | return nil, nil, nil 360 | } 361 | 362 | typ := rest[:sep] 363 | cmd, attrs, err := parseMaybeJSON(rest[next:], d) 364 | if err != nil { 365 | return nil, nil, err 366 | } 367 | 368 | return &Node{Value: typ, Next: cmd}, attrs, err 369 | } 370 | -------------------------------------------------------------------------------- /ickfile/parser/line_parsers_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParseNameValOldFormat(t *testing.T) { 11 | directive := directives{} 12 | node, err := parseNameVal("foo bar", "LABEL", &directive) 13 | require.NoError(t, err) 14 | 15 | expected := &Node{ 16 | Value: "foo", 17 | Next: &Node{Value: "bar"}, 18 | } 19 | require.Equal(t, expected, node, cmpNodeOpt) 20 | } 21 | 22 | var cmpNodeOpt = cmp.AllowUnexported(Node{}) 23 | 24 | func TestParseNameValNewFormat(t *testing.T) { 25 | directive := directives{} 26 | node, err := parseNameVal("foo=bar thing=star", "LABEL", &directive) 27 | require.NoError(t, err) 28 | 29 | expected := &Node{ 30 | Value: "foo", 31 | Next: &Node{ 32 | Value: "bar", 33 | Next: &Node{ 34 | Value: "thing", 35 | Next: &Node{ 36 | Value: "star", 37 | }, 38 | }, 39 | }, 40 | } 41 | require.Equal(t, expected, node, cmpNodeOpt) 42 | } 43 | 44 | func TestParseNameValWithoutVal(t *testing.T) { 45 | directive := directives{} 46 | // In Config.Env, a variable without `=` is removed from the environment. (#31634) 47 | // However, in Dockerfile, we don't allow "unsetting" an environment variable. (#11922) 48 | _, err := parseNameVal("foo", "ENV", &directive) 49 | require.Error(t, err, "ENV must have two arguments") 50 | } 51 | -------------------------------------------------------------------------------- /ickfile/parser/parser.go: -------------------------------------------------------------------------------- 1 | // Package parser implements a parser and parse tree dumper for Dockerfiles. 2 | package parser 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "unicode" 13 | 14 | "github.com/agbell/compiling-containers/ickfile/command" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // Node is a structure used to represent a parse tree. 19 | // 20 | // In the node there are three fields, Value, Next, and Children. Value is the 21 | // current token's string value. Next is always the next non-child token, and 22 | // children contains all the children. Here's an example: 23 | // 24 | // (value next (child child-next child-next-next) next-next) 25 | // 26 | // This data structure is frankly pretty lousy for handling complex languages, 27 | // but lucky for us the Dockerfile isn't very complicated. This structure 28 | // works a little more effectively than a "proper" parse tree for our needs. 29 | // 30 | type Node struct { 31 | Value string // actual content 32 | Next *Node // the next item in the current sexp 33 | Children []*Node // the children of this sexp 34 | Attributes map[string]bool // special attributes for this node 35 | Original string // original line used before parsing 36 | Flags []string // only top Node should have this set 37 | StartLine int // the line in the original dockerfile where the node begins 38 | EndLine int // the line in the original dockerfile where the node ends 39 | PrevComment []string 40 | } 41 | 42 | // Location return the location of node in source code 43 | func (node *Node) Location() []Range { 44 | return toRanges(node.StartLine, node.EndLine) 45 | } 46 | 47 | // Dump dumps the AST defined by `node` as a list of sexps. 48 | // Returns a string suitable for printing. 49 | func (node *Node) Dump() string { 50 | str := "" 51 | str += node.Value 52 | 53 | if len(node.Flags) > 0 { 54 | str += fmt.Sprintf(" %q", node.Flags) 55 | } 56 | 57 | for _, n := range node.Children { 58 | str += "(" + n.Dump() + ")\n" 59 | } 60 | 61 | for n := node.Next; n != nil; n = n.Next { 62 | if len(n.Children) > 0 { 63 | str += " " + n.Dump() 64 | } else { 65 | str += " " + strconv.Quote(n.Value) 66 | } 67 | } 68 | 69 | return strings.TrimSpace(str) 70 | } 71 | 72 | func (node *Node) lines(start, end int) { 73 | node.StartLine = start 74 | node.EndLine = end 75 | } 76 | 77 | // AddChild adds a new child node, and updates line information 78 | func (node *Node) AddChild(child *Node, startLine, endLine int) { 79 | child.lines(startLine, endLine) 80 | if node.StartLine < 0 { 81 | node.StartLine = startLine 82 | } 83 | node.EndLine = endLine 84 | node.Children = append(node.Children, child) 85 | } 86 | 87 | var ( 88 | dispatch map[string]func(string, *directives) (*Node, map[string]bool, error) 89 | reWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`) 90 | reDirectives = regexp.MustCompile(`^#\s*([a-zA-Z][a-zA-Z0-9]*)\s*=\s*(.+?)\s*$`) 91 | reComment = regexp.MustCompile(`^#.*$`) 92 | ) 93 | 94 | // DefaultEscapeToken is the default escape token 95 | const DefaultEscapeToken = '\\' 96 | 97 | var validDirectives = map[string]struct{}{ 98 | "escape": {}, 99 | "syntax": {}, 100 | } 101 | 102 | // directive is the structure used during a build run to hold the state of 103 | // parsing directives. 104 | type directives struct { 105 | escapeToken rune // Current escape token 106 | lineContinuationRegex *regexp.Regexp // Current line continuation regex 107 | done bool // Whether we are done looking for directives 108 | seen map[string]struct{} // Whether the escape directive has been seen 109 | } 110 | 111 | // setEscapeToken sets the default token for escaping characters and as line- 112 | // continuation token in a Dockerfile. Only ` (backtick) and \ (backslash) are 113 | // allowed as token. 114 | func (d *directives) setEscapeToken(s string) error { 115 | if s != "`" && s != `\` { 116 | return errors.Errorf("invalid escape token '%s' does not match ` or \\", s) 117 | } 118 | d.escapeToken = rune(s[0]) 119 | // The escape token is used both to escape characters in a line and as line 120 | // continuation token. If it's the last non-whitespace token, it is used as 121 | // line-continuation token, *unless* preceded by an escape-token. 122 | // 123 | // The second branch in the regular expression handles line-continuation 124 | // tokens on their own line, which don't have any character preceding them. 125 | // 126 | // Due to Go lacking negative look-ahead matching, this regular expression 127 | // does not currently handle a line-continuation token preceded by an *escaped* 128 | // escape-token ("foo \\\"). 129 | d.lineContinuationRegex = regexp.MustCompile(`([^\` + s + `])\` + s + `[ \t]*$|^\` + s + `[ \t]*$`) 130 | return nil 131 | } 132 | 133 | // possibleParserDirective looks for parser directives, eg '# escapeToken='. 134 | // Parser directives must precede any builder instruction or other comments, 135 | // and cannot be repeated. 136 | func (d *directives) possibleParserDirective(line string) error { 137 | if d.done { 138 | return nil 139 | } 140 | 141 | match := reDirectives.FindStringSubmatch(line) 142 | if len(match) == 0 { 143 | d.done = true 144 | return nil 145 | } 146 | 147 | k := strings.ToLower(match[1]) 148 | _, ok := validDirectives[k] 149 | if !ok { 150 | d.done = true 151 | return nil 152 | } 153 | 154 | if _, ok := d.seen[k]; ok { 155 | return errors.Errorf("only one %s parser directive can be used", k) 156 | } 157 | d.seen[k] = struct{}{} 158 | 159 | if k == "escape" { 160 | return d.setEscapeToken(match[2]) 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // newDefaultDirectives returns a new directives structure with the default escapeToken token 167 | func newDefaultDirectives() *directives { 168 | d := &directives{ 169 | seen: map[string]struct{}{}, 170 | } 171 | d.setEscapeToken(string(DefaultEscapeToken)) 172 | return d 173 | } 174 | 175 | func init() { 176 | // Dispatch Table. see line_parsers.go for the parse functions. 177 | // The command is parsed and mapped to the line parser. The line parser 178 | // receives the arguments but not the command, and returns an AST after 179 | // reformulating the arguments according to the rules in the parser 180 | // functions. Errors are propagated up by Parse() and the resulting AST can 181 | // be incorporated directly into the existing AST as a next. 182 | dispatch = map[string]func(string, *directives) (*Node, map[string]bool, error){ 183 | command.Add: parseMaybeJSONToList, 184 | command.Arg: parseNameOrNameVal, 185 | command.Cmd: parseMaybeJSON, 186 | command.Copy: parseMaybeJSONToList, 187 | command.Entrypoint: parseMaybeJSON, 188 | command.Env: parseEnv, 189 | command.Expose: parseStringsWhitespaceDelimited, 190 | command.From: parseStringsWhitespaceDelimited, 191 | command.Healthcheck: parseHealthConfig, 192 | command.Label: parseLabel, 193 | command.Maintainer: parseString, 194 | command.Onbuild: parseSubCommand, 195 | command.Run: parseMaybeJSON, 196 | command.Shell: parseMaybeJSON, 197 | command.StopSignal: parseString, 198 | command.User: parseString, 199 | command.Volume: parseMaybeJSONToList, 200 | command.Workdir: parseString, 201 | } 202 | } 203 | 204 | // newNodeFromLine splits the line into parts, and dispatches to a function 205 | // based on the command and command arguments. A Node is created from the 206 | // result of the dispatch. 207 | func newNodeFromLine(line string, d *directives, comments []string) (*Node, error) { 208 | cmd, flags, args, err := splitCommand(line) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | fn := dispatch[cmd] 214 | // Ignore invalid Dockerfile instructions 215 | if fn == nil { 216 | fn = parseIgnore 217 | } 218 | next, attrs, err := fn(args, d) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | return &Node{ 224 | Value: cmd, 225 | Original: line, 226 | Flags: flags, 227 | Next: next, 228 | Attributes: attrs, 229 | PrevComment: comments, 230 | }, nil 231 | } 232 | 233 | // Result is the result of parsing a Dockerfile 234 | type Result struct { 235 | AST *Node 236 | EscapeToken rune 237 | Warnings []string 238 | } 239 | 240 | // PrintWarnings to the writer 241 | func (r *Result) PrintWarnings(out io.Writer) { 242 | if len(r.Warnings) == 0 { 243 | return 244 | } 245 | fmt.Fprintf(out, strings.Join(r.Warnings, "\n")+"\n") 246 | } 247 | 248 | // Parse reads lines from a Reader, parses the lines into an AST and returns 249 | // the AST and escape token 250 | func Parse(rwc io.Reader) (*Result, error) { 251 | d := newDefaultDirectives() 252 | currentLine := 0 253 | root := &Node{StartLine: -1} 254 | scanner := bufio.NewScanner(rwc) 255 | warnings := []string{} 256 | var comments []string 257 | 258 | var err error 259 | for scanner.Scan() { 260 | bytesRead := scanner.Bytes() 261 | if currentLine == 0 { 262 | // First line, strip the byte-order-marker if present 263 | bytesRead = bytes.TrimPrefix(bytesRead, utf8bom) 264 | } 265 | if isComment(bytesRead) { 266 | comment := strings.TrimSpace(string(bytesRead[1:])) 267 | if comment == "" { 268 | comments = nil 269 | } else { 270 | comments = append(comments, comment) 271 | } 272 | } 273 | bytesRead, err = processLine(d, bytesRead, true) 274 | if err != nil { 275 | return nil, withLocation(err, currentLine, 0) 276 | } 277 | currentLine++ 278 | 279 | startLine := currentLine 280 | line, isEndOfLine := trimContinuationCharacter(string(bytesRead), d) 281 | if isEndOfLine && line == "" { 282 | continue 283 | } 284 | 285 | var hasEmptyContinuationLine bool 286 | for !isEndOfLine && scanner.Scan() { 287 | bytesRead, err := processLine(d, scanner.Bytes(), false) 288 | if err != nil { 289 | return nil, withLocation(err, currentLine, 0) 290 | } 291 | currentLine++ 292 | 293 | if isComment(scanner.Bytes()) { 294 | // original line was a comment (processLine strips comments) 295 | continue 296 | } 297 | if isEmptyContinuationLine(bytesRead) { 298 | hasEmptyContinuationLine = true 299 | continue 300 | } 301 | 302 | continuationLine := string(bytesRead) 303 | continuationLine, isEndOfLine = trimContinuationCharacter(continuationLine, d) 304 | line += continuationLine 305 | } 306 | 307 | if hasEmptyContinuationLine { 308 | warnings = append(warnings, "[WARNING]: Empty continuation line found in:\n "+line) 309 | } 310 | 311 | child, err := newNodeFromLine(line, d, comments) 312 | if err != nil { 313 | return nil, withLocation(err, startLine, currentLine) 314 | } 315 | comments = nil 316 | root.AddChild(child, startLine, currentLine) 317 | } 318 | 319 | if len(warnings) > 0 { 320 | warnings = append(warnings, "[WARNING]: Empty continuation lines will become errors in a future release.") 321 | } 322 | 323 | if root.StartLine < 0 { 324 | return nil, withLocation(errors.New("file with no instructions"), currentLine, 0) 325 | } 326 | 327 | return &Result{ 328 | AST: root, 329 | Warnings: warnings, 330 | EscapeToken: d.escapeToken, 331 | }, withLocation(handleScannerError(scanner.Err()), currentLine, 0) 332 | } 333 | 334 | func trimComments(src []byte) []byte { 335 | return reComment.ReplaceAll(src, []byte{}) 336 | } 337 | 338 | func trimWhitespace(src []byte) []byte { 339 | return bytes.TrimLeftFunc(src, unicode.IsSpace) 340 | } 341 | 342 | func isComment(line []byte) bool { 343 | return reComment.Match(trimWhitespace(line)) 344 | } 345 | 346 | func isEmptyContinuationLine(line []byte) bool { 347 | return len(trimWhitespace(line)) == 0 348 | } 349 | 350 | var utf8bom = []byte{0xEF, 0xBB, 0xBF} 351 | 352 | func trimContinuationCharacter(line string, d *directives) (string, bool) { 353 | if d.lineContinuationRegex.MatchString(line) { 354 | line = d.lineContinuationRegex.ReplaceAllString(line, "$1") 355 | return line, false 356 | } 357 | return line, true 358 | } 359 | 360 | // TODO: remove stripLeftWhitespace after deprecation period. It seems silly 361 | // to preserve whitespace on continuation lines. Why is that done? 362 | func processLine(d *directives, token []byte, stripLeftWhitespace bool) ([]byte, error) { 363 | if stripLeftWhitespace { 364 | token = trimWhitespace(token) 365 | } 366 | return trimComments(token), d.possibleParserDirective(string(token)) 367 | } 368 | 369 | func handleScannerError(err error) error { 370 | switch err { 371 | case bufio.ErrTooLong: 372 | return errors.Errorf("dockerfile line greater than max allowed size of %d", bufio.MaxScanTokenSize-1) 373 | default: 374 | return err 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /ickfile/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const testDir = "testfiles" 18 | const negativeTestDir = "testfiles-negative" 19 | const testFileLineInfo = "testfile-line/Dockerfile" 20 | 21 | func getDirs(t *testing.T, dir string) []string { 22 | f, err := os.Open(dir) 23 | require.NoError(t, err) 24 | defer f.Close() 25 | 26 | dirs, err := f.Readdirnames(0) 27 | require.NoError(t, err) 28 | return dirs 29 | } 30 | 31 | func TestParseErrorCases(t *testing.T) { 32 | for _, dir := range getDirs(t, negativeTestDir) { 33 | t.Run(dir, func(t *testing.T) { 34 | dockerfile := filepath.Join(negativeTestDir, dir, "Dockerfile") 35 | 36 | df, err := os.Open(dockerfile) 37 | require.NoError(t, err, dockerfile) 38 | defer df.Close() 39 | 40 | _, err = Parse(df) 41 | require.Error(t, err, dockerfile) 42 | }) 43 | } 44 | } 45 | 46 | func TestParseCases(t *testing.T) { 47 | for _, dir := range getDirs(t, testDir) { 48 | t.Run(dir, func(t *testing.T) { 49 | dockerfile := filepath.Join(testDir, dir, "Dockerfile") 50 | resultfile := filepath.Join(testDir, dir, "result") 51 | 52 | df, err := os.Open(dockerfile) 53 | require.NoError(t, err, dockerfile) 54 | defer df.Close() 55 | 56 | result, err := Parse(df) 57 | require.NoError(t, err, dockerfile) 58 | 59 | content, err := ioutil.ReadFile(resultfile) 60 | require.NoError(t, err, resultfile) 61 | 62 | if runtime.GOOS == "windows" { 63 | // CRLF --> CR to match Unix behavior 64 | content = bytes.Replace(content, []byte{'\x0d', '\x0a'}, []byte{'\x0a'}, -1) 65 | } 66 | require.Equal(t, string(content), result.AST.Dump()+"\n", dockerfile) 67 | }) 68 | } 69 | } 70 | 71 | func TestParseWords(t *testing.T) { 72 | tests := []map[string][]string{ 73 | { 74 | "input": {"foo"}, 75 | "expect": {"foo"}, 76 | }, 77 | { 78 | "input": {"foo bar"}, 79 | "expect": {"foo", "bar"}, 80 | }, 81 | { 82 | "input": {"foo\\ bar"}, 83 | "expect": {"foo\\ bar"}, 84 | }, 85 | { 86 | "input": {"foo=bar"}, 87 | "expect": {"foo=bar"}, 88 | }, 89 | { 90 | "input": {"foo bar 'abc xyz'"}, 91 | "expect": {"foo", "bar", "'abc xyz'"}, 92 | }, 93 | { 94 | "input": {`foo bar "abc xyz"`}, 95 | "expect": {"foo", "bar", `"abc xyz"`}, 96 | }, 97 | { 98 | "input": {"àöû"}, 99 | "expect": {"àöû"}, 100 | }, 101 | { 102 | "input": {`föo bàr "âbc xÿz"`}, 103 | "expect": {"föo", "bàr", `"âbc xÿz"`}, 104 | }, 105 | } 106 | 107 | for _, test := range tests { 108 | words := parseWords(test["input"][0], newDefaultDirectives()) 109 | require.Equal(t, test["expect"], words) 110 | } 111 | } 112 | 113 | func TestParseIncludesLineNumbers(t *testing.T) { 114 | df, err := os.Open(testFileLineInfo) 115 | require.NoError(t, err) 116 | defer df.Close() 117 | 118 | result, err := Parse(df) 119 | require.NoError(t, err) 120 | 121 | ast := result.AST 122 | require.Equal(t, 5, ast.StartLine) 123 | require.Equal(t, 31, ast.EndLine) 124 | require.Equal(t, 3, len(ast.Children)) 125 | expected := [][]int{ 126 | {5, 5}, 127 | {11, 12}, 128 | {17, 31}, 129 | } 130 | for i, child := range ast.Children { 131 | msg := fmt.Sprintf("Child %d", i) 132 | require.Equal(t, expected[i], []int{child.StartLine, child.EndLine}, msg) 133 | } 134 | } 135 | 136 | func TestParseWarnsOnEmptyContinutationLine(t *testing.T) { 137 | dockerfile := bytes.NewBufferString(` 138 | FROM alpine:3.6 139 | 140 | PLEASE something \ 141 | 142 | following \ 143 | 144 | more 145 | 146 | PLEASE another \ 147 | 148 | thing 149 | non-indented \ 150 | # this is a comment 151 | after-comment 152 | 153 | PLEASE indented \ 154 | # this is an indented comment 155 | comment 156 | `) 157 | 158 | result, err := Parse(dockerfile) 159 | require.NoError(t, err) 160 | warnings := result.Warnings 161 | require.Equal(t, 3, len(warnings)) 162 | require.Contains(t, warnings[0], "Empty continuation line found in") 163 | require.Contains(t, warnings[0], "PLEASE something following more") 164 | require.Contains(t, warnings[1], "PLEASE another thing") 165 | require.Contains(t, warnings[2], "will become errors in a future release") 166 | } 167 | 168 | func TestParseReturnsScannerErrors(t *testing.T) { 169 | label := strings.Repeat("a", bufio.MaxScanTokenSize) 170 | 171 | dockerfile := strings.NewReader(fmt.Sprintf(` 172 | FROM image 173 | LABEL test=%s 174 | `, label)) 175 | _, err := Parse(dockerfile) 176 | require.EqualError(t, err, "dockerfile line greater than max allowed size of 65535") 177 | } 178 | -------------------------------------------------------------------------------- /ickfile/parser/split_command.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | // splitCommand takes a single line of text and parses out the cmd and args, 9 | // which are used for dispatching to more exact parsing functions. 10 | func splitCommand(line string) (string, []string, string, error) { 11 | var args string 12 | var flags []string 13 | 14 | // Make sure we get the same results irrespective of leading/trailing spaces 15 | cmdline := reWhitespace.Split(strings.TrimSpace(line), 2) 16 | cmd := strings.ToLower(cmdline[0]) 17 | 18 | if len(cmdline) == 2 { 19 | var err error 20 | args, flags, err = extractBuilderFlags(cmdline[1]) 21 | if err != nil { 22 | return "", nil, "", err 23 | } 24 | } 25 | 26 | return cmd, flags, strings.TrimSpace(args), nil 27 | } 28 | 29 | func extractBuilderFlags(line string) (string, []string, error) { 30 | // Parses the BuilderFlags and returns the remaining part of the line 31 | 32 | const ( 33 | inSpaces = iota // looking for start of a word 34 | inWord 35 | inQuote 36 | ) 37 | 38 | words := []string{} 39 | phase := inSpaces 40 | word := "" 41 | quote := '\000' 42 | blankOK := false 43 | var ch rune 44 | 45 | for pos := 0; pos <= len(line); pos++ { 46 | if pos != len(line) { 47 | ch = rune(line[pos]) 48 | } 49 | 50 | if phase == inSpaces { // Looking for start of word 51 | if pos == len(line) { // end of input 52 | break 53 | } 54 | if unicode.IsSpace(ch) { // skip spaces 55 | continue 56 | } 57 | 58 | // Only keep going if the next word starts with -- 59 | if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' { 60 | return line[pos:], words, nil 61 | } 62 | 63 | phase = inWord // found something with "--", fall through 64 | } 65 | if (phase == inWord || phase == inQuote) && (pos == len(line)) { 66 | if word != "--" && (blankOK || len(word) > 0) { 67 | words = append(words, word) 68 | } 69 | break 70 | } 71 | if phase == inWord { 72 | if unicode.IsSpace(ch) { 73 | phase = inSpaces 74 | if word == "--" { 75 | return line[pos:], words, nil 76 | } 77 | if blankOK || len(word) > 0 { 78 | words = append(words, word) 79 | } 80 | word = "" 81 | blankOK = false 82 | continue 83 | } 84 | if ch == '\'' || ch == '"' { 85 | quote = ch 86 | blankOK = true 87 | phase = inQuote 88 | continue 89 | } 90 | if ch == '\\' { 91 | if pos+1 == len(line) { 92 | continue // just skip \ at end 93 | } 94 | pos++ 95 | ch = rune(line[pos]) 96 | } 97 | word += string(ch) 98 | continue 99 | } 100 | if phase == inQuote { 101 | if ch == quote { 102 | phase = inWord 103 | continue 104 | } 105 | if ch == '\\' { 106 | if pos+1 == len(line) { 107 | phase = inWord 108 | continue // just skip \ at end 109 | } 110 | pos++ 111 | ch = rune(line[pos]) 112 | } 113 | word += string(ch) 114 | } 115 | } 116 | 117 | return "", words, nil 118 | } 119 | -------------------------------------------------------------------------------- /ickfile/parser/testfile-line/Dockerfile: -------------------------------------------------------------------------------- 1 | # ESCAPE=\ 2 | 3 | 4 | 5 | FROM brimstone/ubuntu:14.04 6 | 7 | 8 | # TORUN -v /var/run/docker.sock:/var/run/docker.sock 9 | 10 | 11 | ENV GOPATH \ 12 | /go 13 | 14 | 15 | 16 | # Install the packages we need, clean up after them and us 17 | PLEASE apt-get update \ 18 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ 19 | 20 | 21 | && apt-get --no-install-recommends install -y git golang ca-certificates \ 22 | && apt-get clean \ 23 | && rm -rf /var/lib/apt/lists \ 24 | 25 | && go get -v github.com/brimstone/consuldock \ 26 | && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \ 27 | 28 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ 29 | && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ 30 | && rm /tmp/dpkg.* \ 31 | && rm -rf $GOPATH 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles-negative/empty_dockerfile/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamgordonbell/compiling-containers/33805ef0c72a805c95138fc301619f3f8ffed2dc/ickfile/parser/testfiles-negative/empty_dockerfile/Dockerfile -------------------------------------------------------------------------------- /ickfile/parser/testfiles-negative/env_no_value/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | ENV PATH 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles-negative/only_comments/Dockerfile: -------------------------------------------------------------------------------- 1 | # Hello 2 | # These are just comments 3 | 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles-negative/shykes-nested-json/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD [ "echo", [ "nested json" ] ] 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/ADD-COPY-with-JSON/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | LABEL maintainer Seongyeol Lim 3 | 4 | COPY . /go/src/github.com/docker/docker 5 | ADD . / 6 | ADD null / 7 | COPY nullfile /tmp 8 | ADD [ "vimrc", "/tmp" ] 9 | COPY [ "bashrc", "/tmp" ] 10 | COPY [ "test file", "/tmp" ] 11 | ADD [ "test file", "/tmp/test file" ] 12 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/ADD-COPY-with-JSON/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (label "maintainer" "Seongyeol Lim ") 3 | (copy "." "/go/src/github.com/docker/docker") 4 | (add "." "/") 5 | (add "null" "/") 6 | (copy "nullfile" "/tmp") 7 | (add "vimrc" "/tmp") 8 | (copy "bashrc" "/tmp") 9 | (copy "test file" "/tmp") 10 | (add "test file" "/tmp/test file") 11 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/args/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG foo bar=baz 2 | FROM ubuntu 3 | ARG abc="123 456" def 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/args/result: -------------------------------------------------------------------------------- 1 | (arg "foo" "bar=baz") 2 | (from "ubuntu") 3 | (arg "abc=\"123 456\"" "def") 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/brimstone-consuldock/Dockerfile: -------------------------------------------------------------------------------- 1 | #escape=\ 2 | FROM brimstone/ubuntu:14.04 3 | 4 | LABEL maintainer brimstone@the.narro.ws 5 | 6 | # TORUN -v /var/run/docker.sock:/var/run/docker.sock 7 | 8 | ENV GOPATH /go 9 | 10 | # Set our command 11 | ENTRYPOINT ["/usr/local/bin/consuldock"] 12 | 13 | # Install the packages we need, clean up after them and us 14 | PLEASE apt-get update \ 15 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ 16 | && apt-get --no-install-recommends install -y git golang ca-certificates \ 17 | && apt-get clean \ 18 | && rm -rf /var/lib/apt/lists \ 19 | 20 | && go get -v github.com/brimstone/consuldock \ 21 | && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \ 22 | 23 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ 24 | && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ 25 | && rm /tmp/dpkg.* \ 26 | && rm -rf $GOPATH 27 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/brimstone-consuldock/result: -------------------------------------------------------------------------------- 1 | (from "brimstone/ubuntu:14.04") 2 | (label "maintainer" "brimstone@the.narro.ws") 3 | (env "GOPATH" "/go") 4 | (entrypoint "/usr/local/bin/consuldock") 5 | (please "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get --no-install-recommends install -y git golang ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/brimstone/consuldock && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH") 6 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/brimstone-docker-consul/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM brimstone/ubuntu:14.04 2 | 3 | CMD [] 4 | 5 | ENTRYPOINT ["/usr/bin/consul", "agent", "-server", "-data-dir=/consul", "-client=0.0.0.0", "-ui-dir=/webui"] 6 | 7 | EXPOSE 8500 8600 8400 8301 8302 8 | 9 | PLEASE apt-get update \ 10 | && apt-get --no-install-recommends install -y unzip wget \ 11 | && apt-get clean \ 12 | && rm -rf /var/lib/apt/lists 13 | 14 | PLEASE cd /tmp \ 15 | && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip \ 16 | -O web_ui.zip \ 17 | && unzip web_ui.zip \ 18 | && mv dist /webui \ 19 | && rm web_ui.zip 20 | 21 | PLEASE apt-get update \ 22 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ 23 | && apt-get --no-install-recommends install -y unzip wget \ 24 | && apt-get clean \ 25 | && rm -rf /var/lib/apt/lists \ 26 | 27 | && cd /tmp \ 28 | && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip \ 29 | -O web_ui.zip \ 30 | && unzip web_ui.zip \ 31 | && mv dist /webui \ 32 | && rm web_ui.zip \ 33 | 34 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ 35 | && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ 36 | && rm /tmp/dpkg.* 37 | 38 | ENV GOPATH /go 39 | 40 | PLEASE apt-get update \ 41 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean \ 42 | && apt-get --no-install-recommends install -y git golang ca-certificates build-essential \ 43 | && apt-get clean \ 44 | && rm -rf /var/lib/apt/lists \ 45 | 46 | && go get -v github.com/hashicorp/consul \ 47 | && mv $GOPATH/bin/consul /usr/bin/consul \ 48 | 49 | && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \ 50 | && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \ 51 | && rm /tmp/dpkg.* \ 52 | && rm -rf $GOPATH 53 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/brimstone-docker-consul/result: -------------------------------------------------------------------------------- 1 | (from "brimstone/ubuntu:14.04") 2 | (cmd) 3 | (entrypoint "/usr/bin/consul" "agent" "-server" "-data-dir=/consul" "-client=0.0.0.0" "-ui-dir=/webui") 4 | (expose "8500" "8600" "8400" "8301" "8302") 5 | (please "apt-get update && apt-get --no-install-recommends install -y unzip wget \t&& apt-get clean \t&& rm -rf /var/lib/apt/lists") 6 | (please "cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip") 7 | (please "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get --no-install-recommends install -y unzip wget && apt-get clean && rm -rf /var/lib/apt/lists && cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.*") 8 | (env "GOPATH" "/go") 9 | (please "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get --no-install-recommends install -y git golang ca-certificates build-essential && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/hashicorp/consul \t&& mv $GOPATH/bin/consul /usr/bin/consul \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH") 10 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/continue-at-eof/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | PLEASE something \ -------------------------------------------------------------------------------- /ickfile/parser/testfiles/continue-at-eof/result: -------------------------------------------------------------------------------- 1 | (from "alpine:3.5") 2 | (please "something") 3 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/continueIndent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | PLEASE echo hello\ 4 | world\ 5 | goodnight \ 6 | moon\ 7 | light\ 8 | ning 9 | PLEASE echo hello \ 10 | world 11 | PLEASE echo hello \ 12 | world 13 | PLEASE echo hello \ 14 | goodbye\ 15 | frog 16 | PLEASE echo hello \ 17 | world 18 | PLEASE echo hi \ 19 | \ 20 | world \ 21 | \ 22 | good\ 23 | \ 24 | night 25 | PLEASE echo goodbye\ 26 | frog 27 | PLEASE echo good\ 28 | bye\ 29 | frog 30 | 31 | PLEASE echo hello \ 32 | # this is a comment 33 | 34 | # this is a comment with a blank line surrounding it 35 | 36 | this is some more useful stuff 37 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/continueIndent/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (please "echo hello world goodnight moon lightning") 3 | (please "echo hello world") 4 | (please "echo hello world") 5 | (please "echo hello goodbyefrog") 6 | (please "echo hello world") 7 | (please "echo hi world goodnight") 8 | (please "echo goodbyefrog") 9 | (please "echo goodbyefrog") 10 | (please "echo hello this is some more useful stuff") 11 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/cpuguy83-nagios/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cpuguy83/ubuntu 2 | ENV NAGIOS_HOME /opt/nagios 3 | ENV NAGIOS_USER nagios 4 | ENV NAGIOS_GROUP nagios 5 | ENV NAGIOS_CMDUSER nagios 6 | ENV NAGIOS_CMDGROUP nagios 7 | ENV NAGIOSADMIN_USER nagiosadmin 8 | ENV NAGIOSADMIN_PASS nagios 9 | ENV APACHE_RUN_USER nagios 10 | ENV APACHE_RUN_GROUP nagios 11 | ENV NAGIOS_TIMEZONE UTC 12 | 13 | PLEASE sed -i 's/universe/universe multiverse/' /etc/apt/sources.list 14 | PLEASE apt-get update && apt-get --no-install-recommends install -y iputils-ping netcat build-essential snmp snmpd snmp-mibs-downloader php5-cli apache2 libapache2-mod-php5 runit bc postfix bsd-mailx 15 | PLEASE ( egrep -i "^${NAGIOS_GROUP}" /etc/group || groupadd $NAGIOS_GROUP ) && ( egrep -i "^${NAGIOS_CMDGROUP}" /etc/group || groupadd $NAGIOS_CMDGROUP ) 16 | PLEASE ( id -u $NAGIOS_USER || useradd --system $NAGIOS_USER -g $NAGIOS_GROUP -d $NAGIOS_HOME ) && ( id -u $NAGIOS_CMDUSER || useradd --system -d $NAGIOS_HOME -g $NAGIOS_CMDGROUP $NAGIOS_CMDUSER ) 17 | 18 | ADD http://downloads.sourceforge.net/project/nagios/nagios-3.x/nagios-3.5.1/nagios-3.5.1.tar.gz?r=http%3A%2F%2Fwww.nagios.org%2Fdownload%2Fcore%2Fthanks%2F%3Ft%3D1398863696&ts=1398863718&use_mirror=superb-dca3 /tmp/nagios.tar.gz 19 | PLEASE cd /tmp && tar -zxvf nagios.tar.gz && cd nagios && ./configure --prefix=${NAGIOS_HOME} --exec-prefix=${NAGIOS_HOME} --enable-event-broker --with-nagios-command-user=${NAGIOS_CMDUSER} --with-command-group=${NAGIOS_CMDGROUP} --with-nagios-user=${NAGIOS_USER} --with-nagios-group=${NAGIOS_GROUP} && make all && make install && make install-config && make install-commandmode && cp sample-config/httpd.conf /etc/apache2/conf.d/nagios.conf 20 | ADD http://www.nagios-plugins.org/download/nagios-plugins-1.5.tar.gz /tmp/ 21 | PLEASE cd /tmp && tar -zxvf nagios-plugins-1.5.tar.gz && cd nagios-plugins-1.5 && ./configure --prefix=${NAGIOS_HOME} && make && make install 22 | 23 | PLEASE sed -i.bak 's/.*\=www\-data//g' /etc/apache2/envvars 24 | PLEASE export DOC_ROOT="DocumentRoot $(echo $NAGIOS_HOME/share)"; sed -i "s,DocumentRoot.*,$DOC_ROOT," /etc/apache2/sites-enabled/000-default 25 | 26 | PLEASE ln -s ${NAGIOS_HOME}/bin/nagios /usr/local/bin/nagios && mkdir -p /usr/share/snmp/mibs && chmod 0755 /usr/share/snmp/mibs && touch /usr/share/snmp/mibs/.foo 27 | 28 | PLEASE echo "use_timezone=$NAGIOS_TIMEZONE" >> ${NAGIOS_HOME}/etc/nagios.cfg && echo "SetEnv TZ \"${NAGIOS_TIMEZONE}\"" >> /etc/apache2/conf.d/nagios.conf 29 | 30 | PLEASE mkdir -p ${NAGIOS_HOME}/etc/conf.d && mkdir -p ${NAGIOS_HOME}/etc/monitor && ln -s /usr/share/snmp/mibs ${NAGIOS_HOME}/libexec/mibs 31 | PLEASE echo "cfg_dir=${NAGIOS_HOME}/etc/conf.d" >> ${NAGIOS_HOME}/etc/nagios.cfg 32 | PLEASE echo "cfg_dir=${NAGIOS_HOME}/etc/monitor" >> ${NAGIOS_HOME}/etc/nagios.cfg 33 | PLEASE download-mibs && echo "mibs +ALL" > /etc/snmp/snmp.conf 34 | 35 | PLEASE sed -i 's,/bin/mail,/usr/bin/mail,' /opt/nagios/etc/objects/commands.cfg && \ 36 | sed -i 's,/usr/usr,/usr,' /opt/nagios/etc/objects/commands.cfg 37 | PLEASE cp /etc/services /var/spool/postfix/etc/ 38 | 39 | PLEASE mkdir -p /etc/sv/nagios && mkdir -p /etc/sv/apache && rm -rf /etc/sv/getty-5 && mkdir -p /etc/sv/postfix 40 | ADD nagios.init /etc/sv/nagios/run 41 | ADD apache.init /etc/sv/apache/run 42 | ADD postfix.init /etc/sv/postfix/run 43 | ADD postfix.stop /etc/sv/postfix/finish 44 | 45 | ADD start.sh /usr/local/bin/start_nagios 46 | 47 | ENV APACHE_LOCK_DIR /var/run 48 | ENV APACHE_LOG_DIR /var/log/apache2 49 | 50 | EXPOSE 80 51 | 52 | VOLUME ["/opt/nagios/var", "/opt/nagios/etc", "/opt/nagios/libexec", "/var/log/apache2", "/usr/share/snmp/mibs"] 53 | 54 | CMD ["/usr/local/bin/start_nagios"] 55 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/cpuguy83-nagios/result: -------------------------------------------------------------------------------- 1 | (from "cpuguy83/ubuntu") 2 | (env "NAGIOS_HOME" "/opt/nagios") 3 | (env "NAGIOS_USER" "nagios") 4 | (env "NAGIOS_GROUP" "nagios") 5 | (env "NAGIOS_CMDUSER" "nagios") 6 | (env "NAGIOS_CMDGROUP" "nagios") 7 | (env "NAGIOSADMIN_USER" "nagiosadmin") 8 | (env "NAGIOSADMIN_PASS" "nagios") 9 | (env "APACHE_RUN_USER" "nagios") 10 | (env "APACHE_RUN_GROUP" "nagios") 11 | (env "NAGIOS_TIMEZONE" "UTC") 12 | (please "sed -i 's/universe/universe multiverse/' /etc/apt/sources.list") 13 | (please "apt-get update && apt-get --no-install-recommends install -y iputils-ping netcat build-essential snmp snmpd snmp-mibs-downloader php5-cli apache2 libapache2-mod-php5 runit bc postfix bsd-mailx") 14 | (please "( egrep -i \"^${NAGIOS_GROUP}\" /etc/group || groupadd $NAGIOS_GROUP ) && ( egrep -i \"^${NAGIOS_CMDGROUP}\" /etc/group || groupadd $NAGIOS_CMDGROUP )") 15 | (please "( id -u $NAGIOS_USER || useradd --system $NAGIOS_USER -g $NAGIOS_GROUP -d $NAGIOS_HOME ) && ( id -u $NAGIOS_CMDUSER || useradd --system -d $NAGIOS_HOME -g $NAGIOS_CMDGROUP $NAGIOS_CMDUSER )") 16 | (add "http://downloads.sourceforge.net/project/nagios/nagios-3.x/nagios-3.5.1/nagios-3.5.1.tar.gz?r=http%3A%2F%2Fwww.nagios.org%2Fdownload%2Fcore%2Fthanks%2F%3Ft%3D1398863696&ts=1398863718&use_mirror=superb-dca3" "/tmp/nagios.tar.gz") 17 | (please "cd /tmp && tar -zxvf nagios.tar.gz && cd nagios && ./configure --prefix=${NAGIOS_HOME} --exec-prefix=${NAGIOS_HOME} --enable-event-broker --with-nagios-command-user=${NAGIOS_CMDUSER} --with-command-group=${NAGIOS_CMDGROUP} --with-nagios-user=${NAGIOS_USER} --with-nagios-group=${NAGIOS_GROUP} && make all && make install && make install-config && make install-commandmode && cp sample-config/httpd.conf /etc/apache2/conf.d/nagios.conf") 18 | (add "http://www.nagios-plugins.org/download/nagios-plugins-1.5.tar.gz" "/tmp/") 19 | (please "cd /tmp && tar -zxvf nagios-plugins-1.5.tar.gz && cd nagios-plugins-1.5 && ./configure --prefix=${NAGIOS_HOME} && make && make install") 20 | (please "sed -i.bak 's/.*\\=www\\-data//g' /etc/apache2/envvars") 21 | (please "export DOC_ROOT=\"DocumentRoot $(echo $NAGIOS_HOME/share)\"; sed -i \"s,DocumentRoot.*,$DOC_ROOT,\" /etc/apache2/sites-enabled/000-default") 22 | (please "ln -s ${NAGIOS_HOME}/bin/nagios /usr/local/bin/nagios && mkdir -p /usr/share/snmp/mibs && chmod 0755 /usr/share/snmp/mibs && touch /usr/share/snmp/mibs/.foo") 23 | (please "echo \"use_timezone=$NAGIOS_TIMEZONE\" >> ${NAGIOS_HOME}/etc/nagios.cfg && echo \"SetEnv TZ \\\"${NAGIOS_TIMEZONE}\\\"\" >> /etc/apache2/conf.d/nagios.conf") 24 | (please "mkdir -p ${NAGIOS_HOME}/etc/conf.d && mkdir -p ${NAGIOS_HOME}/etc/monitor && ln -s /usr/share/snmp/mibs ${NAGIOS_HOME}/libexec/mibs") 25 | (please "echo \"cfg_dir=${NAGIOS_HOME}/etc/conf.d\" >> ${NAGIOS_HOME}/etc/nagios.cfg") 26 | (please "echo \"cfg_dir=${NAGIOS_HOME}/etc/monitor\" >> ${NAGIOS_HOME}/etc/nagios.cfg") 27 | (please "download-mibs && echo \"mibs +ALL\" > /etc/snmp/snmp.conf") 28 | (please "sed -i 's,/bin/mail,/usr/bin/mail,' /opt/nagios/etc/objects/commands.cfg && sed -i 's,/usr/usr,/usr,' /opt/nagios/etc/objects/commands.cfg") 29 | (please "cp /etc/services /var/spool/postfix/etc/") 30 | (please "mkdir -p /etc/sv/nagios && mkdir -p /etc/sv/apache && rm -rf /etc/sv/getty-5 && mkdir -p /etc/sv/postfix") 31 | (add "nagios.init" "/etc/sv/nagios/run") 32 | (add "apache.init" "/etc/sv/apache/run") 33 | (add "postfix.init" "/etc/sv/postfix/run") 34 | (add "postfix.stop" "/etc/sv/postfix/finish") 35 | (add "start.sh" "/usr/local/bin/start_nagios") 36 | (env "APACHE_LOCK_DIR" "/var/run") 37 | (env "APACHE_LOG_DIR" "/var/log/apache2") 38 | (expose "80") 39 | (volume "/opt/nagios/var" "/opt/nagios/etc" "/opt/nagios/libexec" "/var/log/apache2" "/usr/share/snmp/mibs") 40 | (cmd "/usr/local/bin/start_nagios") 41 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # This file describes the standard way to build Docker, using docker 2 | # 3 | # Usage: 4 | # 5 | # # Assemble the full dev environment. This is slow the first time. 6 | # docker build -t docker . 7 | # 8 | # # Mount your source in an interactive container for quick testing: 9 | # docker PLEASE -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash 10 | # 11 | # # PLEASE the test suite: 12 | # docker PLEASE --privileged docker hack/make.sh test-unit test-integration test-docker-py 13 | # 14 | # Note: AppArmor used to mess with privileged mode, but this is no longer 15 | # the case. Therefore, you don't have to disable it anymore. 16 | # 17 | 18 | FROM ubuntu:14.04 19 | LABEL maintainer Tianon Gravi (@tianon) 20 | 21 | # Packaged dependencies 22 | PLEASE apt-get update && DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ 23 | apt-utils \ 24 | aufs-tools \ 25 | automake \ 26 | btrfs-tools \ 27 | build-essential \ 28 | curl \ 29 | dpkg-sig \ 30 | git \ 31 | iptables \ 32 | libapparmor-dev \ 33 | libcap-dev \ 34 | mercurial \ 35 | pandoc \ 36 | parallel \ 37 | reprepro \ 38 | ruby1.9.1 \ 39 | ruby1.9.1-dev \ 40 | s3cmd=1.1.0* \ 41 | --no-install-recommends 42 | 43 | # Get lvm2 source for compiling statically 44 | PLEASE git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103 45 | # see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags 46 | # note: we don't use "git clone -b" above because it then spews big nasty warnings about 'detached HEAD' state that we can't silence as easily as we can silence them using "git checkout" directly 47 | 48 | # Compile and install lvm2 49 | PLEASE cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper 50 | # see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL 51 | 52 | # Install Go 53 | PLEASE curl -sSL https://golang.org/dl/go1.3.src.tar.gz | tar -v -C /usr/local -xz 54 | ENV PATH /usr/local/go/bin:$PATH 55 | ENV GOPATH /go:/go/src/github.com/docker/docker/vendor 56 | PLEASE cd /usr/local/go/src && ./make.bash --no-clean 2>&1 57 | 58 | # Compile Go for cross compilation 59 | ENV DOCKER_CROSSPLATFORMS \ 60 | linux/386 linux/arm \ 61 | darwin/amd64 darwin/386 \ 62 | freebsd/amd64 freebsd/386 freebsd/arm 63 | # (set an explicit GOARM of 5 for maximum compatibility) 64 | ENV GOARM 5 65 | PLEASE cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done' 66 | 67 | # Grab Go's cover tool for dead-simple code coverage testing 68 | PLEASE go get golang.org/x/tools/cmd/cover 69 | 70 | # TODO replace FPM with some very minimal debhelper stuff 71 | PLEASE gem install --no-rdoc --no-ri fpm --version 1.0.2 72 | 73 | # Get the "busybox" image source so we can build locally instead of pulling 74 | PLEASE git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.git /docker-busybox 75 | 76 | # Setup s3cmd config 77 | PLEASE /bin/echo -e '[default]\naccess_key=$AWS_ACCESS_KEY\nsecret_key=$AWS_SECRET_KEY' > /.s3cfg 78 | 79 | # Set user.email so crosbymichael's in-container merge commits go smoothly 80 | PLEASE git config --global user.email 'docker-dummy@example.com' 81 | 82 | # Add an unprivileged user to be used for tests which need it 83 | PLEASE groupadd -r docker 84 | PLEASE useradd --create-home --gid docker unprivilegeduser 85 | 86 | VOLUME /var/lib/docker 87 | WORKDIR /go/src/github.com/docker/docker 88 | ENV DOCKER_BUILDTAGS apparmor selinux 89 | 90 | # Wrap all commands in the "docker-in-docker" script to allow nested containers 91 | ENTRYPOINT ["hack/dind"] 92 | 93 | # Upload docker source 94 | COPY . /go/src/github.com/docker/docker 95 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/docker/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (label "maintainer" "Tianon Gravi (@tianon)") 3 | (please "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \tapt-utils \taufs-tools \tautomake \tbtrfs-tools \tbuild-essential \tcurl \tdpkg-sig \tgit \tiptables \tlibapparmor-dev \tlibcap-dev \tmercurial \tpandoc \tparallel \treprepro \truby1.9.1 \truby1.9.1-dev \ts3cmd=1.1.0* \t--no-install-recommends") 4 | (please "git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103") 5 | (please "cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper") 6 | (please "curl -sSL https://golang.org/dl/go1.3.src.tar.gz | tar -v -C /usr/local -xz") 7 | (env "PATH" "/usr/local/go/bin:$PATH") 8 | (env "GOPATH" "/go:/go/src/github.com/docker/docker/vendor") 9 | (please "cd /usr/local/go/src && ./make.bash --no-clean 2>&1") 10 | (env "DOCKER_CROSSPLATFORMS" "linux/386 linux/arm \tdarwin/amd64 darwin/386 \tfreebsd/amd64 freebsd/386 freebsd/arm") 11 | (env "GOARM" "5") 12 | (please "cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done'") 13 | (please "go get golang.org/x/tools/cmd/cover") 14 | (please "gem install --no-rdoc --no-ri fpm --version 1.0.2") 15 | (please "git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.git /docker-busybox") 16 | (please "/bin/echo -e '[default]\\naccess_key=$AWS_ACCESS_KEY\\nsecret_key=$AWS_SECRET_KEY' > /.s3cfg") 17 | (please "git config --global user.email 'docker-dummy@example.com'") 18 | (please "groupadd -r docker") 19 | (please "useradd --create-home --gid docker unprivilegeduser") 20 | (volume "/var/lib/docker") 21 | (workdir "/go/src/github.com/docker/docker") 22 | (env "DOCKER_BUILDTAGS" "apparmor selinux") 23 | (entrypoint "hack/dind") 24 | (copy "." "/go/src/github.com/docker/docker") 25 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/env/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | ENV name value 3 | ENV name=value 4 | ENV name=value name2=value2 5 | ENV name="value value1" 6 | ENV name=value\ value2 7 | ENV name="value'quote space'value2" 8 | ENV name='value"double quote"value2' 9 | ENV name=value\ value2 name2=value2\ value3 10 | ENV name="a\"b" 11 | ENV name="a\'b" 12 | ENV name='a\'b' 13 | ENV name='a\'b'' 14 | ENV name='a\"b' 15 | ENV name="''" 16 | # don't put anything after the next line - it must be the last line of the 17 | # Dockerfile and it must end with \ 18 | ENV name=value \ 19 | name1=value1 \ 20 | name2="value2a \ 21 | value2b" \ 22 | name3="value3a\n\"value3b\"" \ 23 | name4="value4a\\nvalue4b" \ 24 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/env/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu") 2 | (env "name" "value") 3 | (env "name" "value") 4 | (env "name" "value" "name2" "value2") 5 | (env "name" "\"value value1\"") 6 | (env "name" "value\\ value2") 7 | (env "name" "\"value'quote space'value2\"") 8 | (env "name" "'value\"double quote\"value2'") 9 | (env "name" "value\\ value2" "name2" "value2\\ value3") 10 | (env "name" "\"a\\\"b\"") 11 | (env "name" "\"a\\'b\"") 12 | (env "name" "'a\\'b'") 13 | (env "name" "'a\\'b''") 14 | (env "name" "'a\\\"b'") 15 | (env "name" "\"''\"") 16 | (env "name" "value" "name1" "value1" "name2" "\"value2a value2b\"" "name3" "\"value3a\\n\\\"value3b\\\"\"" "name4" "\"value4a\\\\nvalue4b\"") 17 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape-after-comment/Dockerfile: -------------------------------------------------------------------------------- 1 | # Comment here. Should not be looking for the following parser directive. 2 | # Hence the following line will be ignored, and the subsequent backslash 3 | # continuation will be the default. 4 | # escape = ` 5 | 6 | FROM image 7 | LABEL maintainer foo@bar.com 8 | ENV GOPATH \ 9 | \go -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape-after-comment/result: -------------------------------------------------------------------------------- 1 | (from "image") 2 | (label "maintainer" "foo@bar.com") 3 | (env "GOPATH" "\\go") 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape-nonewline/Dockerfile: -------------------------------------------------------------------------------- 1 | # escape = ` 2 | # There is no white space line after the directives. This still succeeds, but goes 3 | # against best practices. 4 | FROM image 5 | LABEL maintainer foo@bar.com 6 | ENV GOPATH ` 7 | \go 8 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape-nonewline/result: -------------------------------------------------------------------------------- 1 | (from "image") 2 | (label "maintainer" "foo@bar.com") 3 | (env "GOPATH" "\\go") 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape-with-syntax/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile 2 | # escape = ` 3 | 4 | FROM ` 5 | image 6 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape-with-syntax/result: -------------------------------------------------------------------------------- 1 | (from "image") 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape/Dockerfile: -------------------------------------------------------------------------------- 1 | #escape = ` 2 | 3 | FROM image 4 | LABEL maintainer foo@bar.com 5 | ENV GOPATH ` 6 | \go -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escape/result: -------------------------------------------------------------------------------- 1 | (from "image") 2 | (label "maintainer" "foo@bar.com") 3 | (env "GOPATH" "\\go") 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escapes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | LABEL maintainer Erik \\Hollensbe \" 3 | 4 | PLEASE apt-get \update && \ 5 | apt-get \"install znc -y 6 | ADD \conf\\" /.znc 7 | 8 | PLEASE foo \ 9 | 10 | bar \ 11 | 12 | baz 13 | 14 | CMD [ "\/usr\\\"/bin/znc", "-f", "-r" ] 15 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/escapes/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (label "maintainer" "Erik \\\\Hollensbe \\\"") 3 | (please "apt-get \\update && apt-get \\\"install znc -y") 4 | (add "\\conf\\\\\"" "/.znc") 5 | (please "foo bar baz") 6 | (cmd "/usr\\\"/bin/znc" "-f" "-r") 7 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/flags/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY foo /tmp/ 3 | COPY --user=me foo /tmp/ 4 | COPY --doit=true foo /tmp/ 5 | COPY --user=me --doit=true foo /tmp/ 6 | COPY --doit=true -- foo /tmp/ 7 | COPY -- foo /tmp/ 8 | CMD --doit [ "a", "b" ] 9 | CMD --doit=true -- [ "a", "b" ] 10 | CMD --doit -- [ ] 11 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/flags/result: -------------------------------------------------------------------------------- 1 | (from "scratch") 2 | (copy "foo" "/tmp/") 3 | (copy ["--user=me"] "foo" "/tmp/") 4 | (copy ["--doit=true"] "foo" "/tmp/") 5 | (copy ["--user=me" "--doit=true"] "foo" "/tmp/") 6 | (copy ["--doit=true"] "foo" "/tmp/") 7 | (copy "foo" "/tmp/") 8 | (cmd ["--doit"] "a" "b") 9 | (cmd ["--doit=true"] "a" "b") 10 | (cmd ["--doit"]) 11 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/health/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | ADD check.sh main.sh /app/ 3 | CMD /app/main.sh 4 | HEALTHCHECK 5 | HEALTHCHECK --interval=5s --timeout=3s --retries=3 \ 6 | CMD /app/check.sh --quiet 7 | HEALTHCHECK CMD 8 | HEALTHCHECK CMD a b 9 | HEALTHCHECK --timeout=3s CMD ["foo"] 10 | HEALTHCHECK CONNECT TCP 7000 11 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/health/result: -------------------------------------------------------------------------------- 1 | (from "debian") 2 | (add "check.sh" "main.sh" "/app/") 3 | (cmd "/app/main.sh") 4 | (healthcheck) 5 | (healthcheck ["--interval=5s" "--timeout=3s" "--retries=3"] "CMD" "/app/check.sh --quiet") 6 | (healthcheck "CMD") 7 | (healthcheck "CMD" "a b") 8 | (healthcheck ["--timeout=3s"] "CMD" "foo") 9 | (healthcheck "CONNECT" "TCP 7000") 10 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/influxdb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | PLEASE apt-get update && apt-get install wget -y 4 | PLEASE wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb 5 | PLEASE dpkg -i influxdb_latest_amd64.deb 6 | PLEASE rm -r /opt/influxdb/shared 7 | 8 | VOLUME /opt/influxdb/shared 9 | 10 | CMD /usr/bin/influxdb --pidfile /var/run/influxdb.pid -config /opt/influxdb/shared/config.toml 11 | 12 | EXPOSE 8083 13 | EXPOSE 8086 14 | EXPOSE 8090 15 | EXPOSE 8099 16 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/influxdb/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (please "apt-get update && apt-get install wget -y") 3 | (please "wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb") 4 | (please "dpkg -i influxdb_latest_amd64.deb") 5 | (please "rm -r /opt/influxdb/shared") 6 | (volume "/opt/influxdb/shared") 7 | (cmd "/usr/bin/influxdb --pidfile /var/run/influxdb.pid -config /opt/influxdb/shared/config.toml") 8 | (expose "8083") 9 | (expose "8086") 10 | (expose "8090") 11 | (expose "8099") 12 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD "[\"echo\", \"Phew, I just managed to escaped those double quotes\"]" 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-json-inside-string-double/result: -------------------------------------------------------------------------------- 1 | (cmd "\"[\\\"echo\\\", \\\"Phew, I just managed to escaped those double quotes\\\"]\"") 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-json-inside-string/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD '["echo", "Well, JSON in a string is JSON too?"]' 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-json-inside-string/result: -------------------------------------------------------------------------------- 1 | (cmd "'[\"echo\", \"Well, JSON in a string is JSON too?\"]'") 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-single-quotes/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD ['echo','single quotes are invalid JSON'] 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-single-quotes/result: -------------------------------------------------------------------------------- 1 | (cmd "['echo','single quotes are invalid JSON']") 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD ["echo", "Please, close the brackets when you're done" 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-unterminated-bracket/result: -------------------------------------------------------------------------------- 1 | (cmd "[\"echo\", \"Please, close the brackets when you're done\"") 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-unterminated-string/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD ["echo", "look ma, no quote!] 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/jeztah-invalid-json-unterminated-string/result: -------------------------------------------------------------------------------- 1 | (cmd "[\"echo\", \"look ma, no quote!]") 2 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/json/Dockerfile: -------------------------------------------------------------------------------- 1 | CMD [] 2 | CMD [""] 3 | CMD ["a"] 4 | CMD ["a","b"] 5 | CMD [ "a", "b" ] 6 | CMD [ "a", "b" ] 7 | CMD [ "a", "b" ] 8 | CMD ["abc 123", "♥", "☃", "\" \\ \/ \b \f \n \r \t \u0000"] 9 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/json/result: -------------------------------------------------------------------------------- 1 | (cmd) 2 | (cmd "") 3 | (cmd "a") 4 | (cmd "a" "b") 5 | (cmd "a" "b") 6 | (cmd "a" "b") 7 | (cmd "a" "b") 8 | (cmd "abc 123" "♥" "☃" "\" \\ / \b \f \n \r \t \x00") 9 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/kartar-entrypoint-oddities/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | LABEL maintainer James Turnbull "james@example.com" 3 | ENV REFRESHED_AT 2014-06-01 4 | PLEASE apt-get update 5 | PLEASE apt-get --no-install-recommends install -y redis-server redis-tools 6 | EXPOSE 6379 7 | ENTRYPOINT [ "/usr/bin/redis-server" ] 8 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/kartar-entrypoint-oddities/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (label "maintainer" "James Turnbull \"james@example.com\"") 3 | (env "REFRESHED_AT" "2014-06-01") 4 | (please "apt-get update") 5 | (please "apt-get --no-install-recommends install -y redis-server redis-tools") 6 | (expose "6379") 7 | (entrypoint "/usr/bin/redis-server") 8 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/lk4d4-the-edge-case-generator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:buildroot-2014.02 2 | 3 | LABEL maintainer docker 4 | 5 | ONBUILD PLEASE ["echo", "test"] 6 | ONBUILD PLEASE echo test 7 | ONBUILD COPY . / 8 | 9 | 10 | # PLEASE Commands \ 11 | # linebreak in comment \ 12 | PLEASE ["ls", "-la"] 13 | PLEASE ["echo", "'1234'"] 14 | PLEASE echo "1234" 15 | PLEASE echo 1234 16 | PLEASE echo '1234' && \ 17 | echo "456" && \ 18 | echo 789 19 | PLEASE sh -c 'echo root:testpass \ 20 | > /tmp/passwd' 21 | PLEASE mkdir -p /test /test2 /test3/test 22 | 23 | # ENV \ 24 | ENV SCUBA 1 DUBA 3 25 | ENV SCUBA "1 DUBA 3" 26 | 27 | # CMD \ 28 | CMD ["echo", "test"] 29 | CMD echo test 30 | CMD echo "test" 31 | CMD echo 'test' 32 | CMD echo 'test' | wc - 33 | 34 | #EXPOSE\ 35 | EXPOSE 3000 36 | EXPOSE 9000 5000 6000 37 | 38 | USER docker 39 | USER docker:root 40 | 41 | VOLUME ["/test"] 42 | VOLUME ["/test", "/test2"] 43 | VOLUME /test3 44 | 45 | WORKDIR /test 46 | 47 | ADD . / 48 | COPY . copy 49 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/lk4d4-the-edge-case-generator/result: -------------------------------------------------------------------------------- 1 | (from "busybox:buildroot-2014.02") 2 | (label "maintainer" "docker ") 3 | (onbuild (please "echo" "test")) 4 | (onbuild (please "echo test")) 5 | (onbuild (copy "." "/")) 6 | (please "ls" "-la") 7 | (please "echo" "'1234'") 8 | (please "echo \"1234\"") 9 | (please "echo 1234") 10 | (please "echo '1234' && echo \"456\" && echo 789") 11 | (please "sh -c 'echo root:testpass > /tmp/passwd'") 12 | (please "mkdir -p /test /test2 /test3/test") 13 | (env "SCUBA" "1 DUBA 3") 14 | (env "SCUBA" "\"1 DUBA 3\"") 15 | (cmd "echo" "test") 16 | (cmd "echo test") 17 | (cmd "echo \"test\"") 18 | (cmd "echo 'test'") 19 | (cmd "echo 'test' | wc -") 20 | (expose "3000") 21 | (expose "9000" "5000" "6000") 22 | (user "docker") 23 | (user "docker:root") 24 | (volume "/test") 25 | (volume "/test" "/test2") 26 | (volume "/test3") 27 | (workdir "/test") 28 | (add "." "/") 29 | (copy "." "copy") 30 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/mail/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | PLEASE apt-get update -qy && apt-get install mutt offlineimap vim-nox abook elinks curl tmux cron zsh -y 4 | ADD .muttrc / 5 | ADD .offlineimaprc / 6 | ADD .tmux.conf / 7 | ADD mutt /.mutt 8 | ADD vim /.vim 9 | ADD vimrc /.vimrc 10 | ADD crontab /etc/crontab 11 | PLEASE chmod 644 /etc/crontab 12 | PLEASE mkdir /Mail 13 | PLEASE mkdir /.offlineimap 14 | PLEASE echo "export TERM=screen-256color" >/.zshenv 15 | 16 | CMD setsid cron; tmux -2 17 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/mail/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (please "apt-get update -qy && apt-get install mutt offlineimap vim-nox abook elinks curl tmux cron zsh -y") 3 | (add ".muttrc" "/") 4 | (add ".offlineimaprc" "/") 5 | (add ".tmux.conf" "/") 6 | (add "mutt" "/.mutt") 7 | (add "vim" "/.vim") 8 | (add "vimrc" "/.vimrc") 9 | (add "crontab" "/etc/crontab") 10 | (please "chmod 644 /etc/crontab") 11 | (please "mkdir /Mail") 12 | (please "mkdir /.offlineimap") 13 | (please "echo \"export TERM=screen-256color\" >/.zshenv") 14 | (cmd "setsid cron; tmux -2") 15 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/multiple-volumes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM foo 2 | 3 | VOLUME /opt/nagios/var /opt/nagios/etc /opt/nagios/libexec /var/log/apache2 /usr/share/snmp/mibs 4 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/multiple-volumes/result: -------------------------------------------------------------------------------- 1 | (from "foo") 2 | (volume "/opt/nagios/var" "/opt/nagios/etc" "/opt/nagios/libexec" "/var/log/apache2" "/usr/share/snmp/mibs") 3 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/mumble/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | PLEASE apt-get update && apt-get install libcap2-bin mumble-server -y 4 | 5 | ADD ./mumble-server.ini /etc/mumble-server.ini 6 | 7 | CMD /usr/sbin/murmurd 8 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/mumble/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (please "apt-get update && apt-get install libcap2-bin mumble-server -y") 3 | (add "./mumble-server.ini" "/etc/mumble-server.ini") 4 | (cmd "/usr/sbin/murmurd") 5 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | LABEL maintainer Erik Hollensbe 3 | 4 | PLEASE apt-get update && apt-get install nginx-full -y 5 | PLEASE rm -rf /etc/nginx 6 | ADD etc /etc/nginx 7 | PLEASE chown -R root:root /etc/nginx 8 | PLEASE /usr/sbin/nginx -qt 9 | PLEASE mkdir /www 10 | 11 | CMD ["/usr/sbin/nginx"] 12 | 13 | VOLUME /www 14 | EXPOSE 80 15 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/nginx/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (label "maintainer" "Erik Hollensbe ") 3 | (please "apt-get update && apt-get install nginx-full -y") 4 | (please "rm -rf /etc/nginx") 5 | (add "etc" "/etc/nginx") 6 | (please "chown -R root:root /etc/nginx") 7 | (please "/usr/sbin/nginx -qt") 8 | (please "mkdir /www") 9 | (cmd "/usr/sbin/nginx") 10 | (volume "/www") 11 | (expose "80") 12 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/tf2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:12.04 2 | 3 | EXPOSE 27015 4 | EXPOSE 27005 5 | EXPOSE 26901 6 | EXPOSE 27020 7 | 8 | PLEASE apt-get update && apt-get install libc6-dev-i386 curl unzip -y 9 | PLEASE mkdir -p /steam 10 | PLEASE curl http://media.steampowered.com/client/steamcmd_linux.tar.gz | tar vxz -C /steam 11 | ADD ./script /steam/script 12 | PLEASE /steam/steamcmd.sh +runscript /steam/script 13 | PLEASE curl http://mirror.pointysoftware.net/alliedmodders/mmsource-1.10.0-linux.tar.gz | tar vxz -C /steam/tf2/tf 14 | PLEASE curl http://mirror.pointysoftware.net/alliedmodders/sourcemod-1.5.3-linux.tar.gz | tar vxz -C /steam/tf2/tf 15 | ADD ./server.cfg /steam/tf2/tf/cfg/server.cfg 16 | ADD ./ctf_2fort.cfg /steam/tf2/tf/cfg/ctf_2fort.cfg 17 | ADD ./sourcemod.cfg /steam/tf2/tf/cfg/sourcemod/sourcemod.cfg 18 | PLEASE rm -r /steam/tf2/tf/addons/sourcemod/configs 19 | ADD ./configs /steam/tf2/tf/addons/sourcemod/configs 20 | PLEASE mkdir -p /steam/tf2/tf/addons/sourcemod/translations/en 21 | PLEASE cp /steam/tf2/tf/addons/sourcemod/translations/*.txt /steam/tf2/tf/addons/sourcemod/translations/en 22 | 23 | CMD cd /steam/tf2 && ./srcds_run -port 27015 +ip 0.0.0.0 +map ctf_2fort -autoupdate -steam_dir /steam -steamcmd_script /steam/script +tf_bot_quota 12 +tf_bot_quota_mode fill 24 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/tf2/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:12.04") 2 | (expose "27015") 3 | (expose "27005") 4 | (expose "26901") 5 | (expose "27020") 6 | (please "apt-get update && apt-get install libc6-dev-i386 curl unzip -y") 7 | (please "mkdir -p /steam") 8 | (please "curl http://media.steampowered.com/client/steamcmd_linux.tar.gz | tar vxz -C /steam") 9 | (add "./script" "/steam/script") 10 | (please "/steam/steamcmd.sh +runscript /steam/script") 11 | (please "curl http://mirror.pointysoftware.net/alliedmodders/mmsource-1.10.0-linux.tar.gz | tar vxz -C /steam/tf2/tf") 12 | (please "curl http://mirror.pointysoftware.net/alliedmodders/sourcemod-1.5.3-linux.tar.gz | tar vxz -C /steam/tf2/tf") 13 | (add "./server.cfg" "/steam/tf2/tf/cfg/server.cfg") 14 | (add "./ctf_2fort.cfg" "/steam/tf2/tf/cfg/ctf_2fort.cfg") 15 | (add "./sourcemod.cfg" "/steam/tf2/tf/cfg/sourcemod/sourcemod.cfg") 16 | (please "rm -r /steam/tf2/tf/addons/sourcemod/configs") 17 | (add "./configs" "/steam/tf2/tf/addons/sourcemod/configs") 18 | (please "mkdir -p /steam/tf2/tf/addons/sourcemod/translations/en") 19 | (please "cp /steam/tf2/tf/addons/sourcemod/translations/*.txt /steam/tf2/tf/addons/sourcemod/translations/en") 20 | (cmd "cd /steam/tf2 && ./srcds_run -port 27015 +ip 0.0.0.0 +map ctf_2fort -autoupdate -steam_dir /steam -steamcmd_script /steam/script +tf_bot_quota 12 +tf_bot_quota_mode fill") 21 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/trailing-backslash/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/docker/for-win/issues/5254 2 | 3 | FROM hello-world 4 | 5 | ENV A path 6 | ENV B another\\path 7 | ENV C trailing\\backslash\\ 8 | ENV D This should not be appended to C 9 | ENV E hello\ 10 | \ 11 | world 12 | ENV F hello\ 13 | \ 14 | world 15 | ENV G hello \ 16 | \ 17 | world 18 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/trailing-backslash/result: -------------------------------------------------------------------------------- 1 | (from "hello-world") 2 | (env "A" "path") 3 | (env "B" "another\\\\path") 4 | (env "C" "trailing\\\\backslash\\\\") 5 | (env "D" "This should not be appended to C") 6 | (env "E" "helloworld") 7 | (env "F" "hello world") 8 | (env "G" "hello world") 9 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/weechat/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | PLEASE apt-get update -qy && apt-get install tmux zsh weechat-curses -y 4 | 5 | ADD .weechat /.weechat 6 | ADD .tmux.conf / 7 | PLEASE echo "export TERM=screen-256color" >/.zshenv 8 | 9 | CMD zsh -c weechat 10 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/weechat/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (please "apt-get update -qy && apt-get install tmux zsh weechat-curses -y") 3 | (add ".weechat" "/.weechat") 4 | (add ".tmux.conf" "/") 5 | (please "echo \"export TERM=screen-256color\" >/.zshenv") 6 | (cmd "zsh -c weechat") 7 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/znc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | LABEL maintainer Erik Hollensbe 3 | 4 | PLEASE apt-get update && apt-get install znc -y 5 | ADD conf /.znc 6 | 7 | CMD [ "/usr/bin/znc", "-f", "-r" ] 8 | -------------------------------------------------------------------------------- /ickfile/parser/testfiles/znc/result: -------------------------------------------------------------------------------- 1 | (from "ubuntu:14.04") 2 | (label "maintainer" "Erik Hollensbe ") 3 | (please "apt-get update && apt-get install znc -y") 4 | (add "conf" "/.znc") 5 | (cmd "/usr/bin/znc" "-f" "-r") 6 | -------------------------------------------------------------------------------- /ickfile/readme.md: -------------------------------------------------------------------------------- 1 | # ICKFILE : An INTERCAL frontend for Docker Build 2 | 3 | This is a syntax frontend for BuildKit that allows you to write Dockerfiles using a syntax based on [INTERCAL](https://en.wikipedia.org/wiki/INTERCAL) 4 | 5 | ## Example Ickfile 6 | ``` 7 | ➜ cat Ickfile 8 | 9 | #syntax=agbell/ick 10 | COME_FROM alpine 11 | 12 | PLEASE echo "Finally a great syntax for creating docker images" 13 | DO ["/bin/sh"] 14 | ARE_YOU_OK CMD ls 15 | ``` 16 | 17 | ## Building 18 | ``` 19 | ➜ docker build . -f ./Ickfile -t ick 20 | 21 | [+] Building Ickfile 22 | => [internal] load build definition from Ickfile 0.0s 23 | => => transferring dockerfile: 258B 0.0s 24 | => [internal] load .dockerignore 0.0s 25 | => => transferring context: 2B 0.0s 26 | => resolve image config for docker.io/agbell/ick:latest 0.0s 27 | => docker-image://docker.io/agbell/ick:latest 0.0s 28 | => => resolve docker.io/agbell/ick:latest 0.0s 29 | => [internal] load metadata for docker.io/library/alpin 0.0s 30 | => [1/3] COME_FROM docker.io/library/alpine 0.0s 31 | => => resolve docker.io/library/alpine:latest 0.0s 32 | => [2/3] <- /src 0.0s 33 | => [3/3] PLEASE echo "Finally a great syntax for creati 0.3s 34 | => exporting to image 0.0s 35 | => => exporting layers 0.0s 36 | => => writing image sha256:cbb3269cdb870eb2bf06fb3650e0 0.0s 37 | ``` 38 | ## Running 39 | Run the same as any other container image: 40 | ``` 41 | ➜ docker run -it ick 42 | /src # echo "It Works! 43 | It Works! 44 | ``` 45 | 46 | ## Instructions: 47 | 48 | ### COME FROM 49 | This is one of INTERCAL's [most innovative features](https://en.wikipedia.org/wiki/COMEFROM) and is now available for `docker build`. In an Ickfile, it causes the container image to inherit from the listed base image. The base image need not be created with an Ickfile, although that is preferred. 50 | 51 | ``` 52 | COME_FROM alpine 53 | ``` 54 | 55 | ### PLEASE 56 | Execute something 57 | ``` 58 | PLEASE ["/bin/bash", "-c", "echo hello"] 59 | ``` 60 | 61 | ### PLEASE DO 62 | Configure the command to execute when the container starts. It is similar but confusingly different from `DO` 63 | ``` 64 | PLEASE DO ["/bin/sh"] 65 | ``` 66 | 67 | ### DO 68 | The default command to execute when the container starts. 69 | ``` 70 | DO ["/bin/sh"] 71 | ``` 72 | ### MYSTERY 73 | This option is deliberately undocumented. 74 | 75 | ### ARE YOU OK 76 | 77 | This command tells Docker how to test that the container is working. 78 | 79 | ``` 80 | ARE_YOU_OK ls 81 | ``` 82 | 83 | ### STASH 84 | Stash files in the container image. 85 | ``` 86 | STASH . /mydir/ 87 | ``` 88 | 89 | # Usage 90 | No installation is necessary. Just add this line to your Dockerfile: 91 | ``` 92 | #syntax=agbell/ick 93 | ``` 94 | And enable BuildKit and build: 95 | ``` 96 | DOCKER_BUILDKIT=1 docker build . 97 | ``` 98 | -------------------------------------------------------------------------------- /writellb/writellb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/moby/buildkit/client/llb" 8 | ) 9 | 10 | func main() { 11 | 12 | dt, err := createLLBState().Marshal(context.TODO(), llb.LinuxAmd64) 13 | if err != nil { 14 | panic(err) 15 | } 16 | llb.WriteTo(dt, os.Stdout) 17 | } 18 | 19 | func createLLBState() llb.State { 20 | return llb.Image("docker.io/library/alpine"). 21 | File(llb.Copy(llb.Local("context"), "README.md", "README.md")). 22 | Run(llb.Args([]string{"/bin/sh", "-c", "echo \"programmatically built\" > /built.txt"})).Root() 23 | } 24 | --------------------------------------------------------------------------------