Skip to content

Commit 3275c5b

Browse files
authored
Merge pull request #104 from m-ildefons/gh-95
parser: allow --mount arguments in any order
2 parents df1fe26 + 15e41ba commit 3275c5b

File tree

2 files changed

+122
-59
lines changed

2 files changed

+122
-59
lines changed

src/Language/Docker/Parser/Run.hs

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ data RunMountArg
2828
| MountArgSharing CacheSharing
2929
| MountArgSource SourcePath
3030
| MountArgTarget TargetPath
31-
| MountArgType Text
31+
| MountArgType MountType
3232
| MountArgUid Text
3333
| MountArgGid Text
3434
deriving (Show)
@@ -39,6 +39,7 @@ data MountType
3939
| Tmpfs
4040
| Secret
4141
| Ssh
42+
deriving (Show)
4243

4344
parseRun :: (?esc :: Char) => Parser (Instruction Text)
4445
parseRun = do
@@ -83,32 +84,29 @@ runFlagNetwork = do
8384
runFlagMount :: (?esc :: Char) => Parser RunMount
8485
runFlagMount = do
8586
void $ string "--mount="
86-
maybeType <-
87-
choice
88-
[ string "type="
89-
*> choice
90-
[ Just Bind <$ string "bind",
91-
Just Cache <$ string "cache",
92-
Just Tmpfs <$ string "tmpfs",
93-
Just Secret <$ string "secret",
94-
Just Ssh <$ string "ssh"
95-
],
96-
pure Nothing
97-
]
98-
(mountType, args) <- return $
99-
case maybeType of
100-
Nothing -> (Bind, argsParser Bind)
101-
Just Ssh -> (Ssh, choice [string "," *> argsParser Ssh, pure []])
102-
Just t -> (t, string "," *> argsParser t)
103-
case mountType of
104-
Bind -> BindMount <$> (bindMount =<< args)
105-
Cache -> CacheMount <$> (cacheMount =<< args)
106-
Tmpfs -> TmpfsMount <$> (tmpfsMount =<< args)
107-
Secret -> SecretMount <$> (secretMount =<< args)
108-
Ssh -> SshMount <$> (secretMount =<< args)
109-
110-
argsParser :: (?esc :: Char) => MountType -> Parser [RunMountArg]
111-
argsParser mountType = mountChoices mountType `sepBy1` string ","
87+
args <- mountArgs `sepBy1` string ","
88+
mt <- parseTypeFromArgs args
89+
case mt of
90+
Bind -> BindMount <$> (bindMount $ filter (not . isMountArgType) args)
91+
Cache -> CacheMount <$> (cacheMount $ filter (not . isMountArgType) args)
92+
Tmpfs -> TmpfsMount <$> (tmpfsMount $ filter (not . isMountArgType) args)
93+
Secret -> SecretMount <$> (secretMount $ filter (not . isMountArgType) args)
94+
Ssh -> SshMount <$> (secretMount $ filter (not . isMountArgType) args)
95+
96+
parseTypeFromArgs :: [RunMountArg] -> Parser MountType
97+
parseTypeFromArgs args =
98+
-- `notFollowedBy eof` is a trivially succeeding parser that isn't supposed to
99+
-- consume any input here. It's a hack that converts a simple type into a
100+
-- parser. This allows this function to emit parse errors after analyzing its
101+
-- input arguments and not consume any input.
102+
case filter isMountArgType args of
103+
[] -> Bind <$ notFollowedBy eof
104+
[(MountArgType t)] -> t <$ notFollowedBy eof
105+
_:_ -> fail $ "--mount with multiple `type` arguments"
106+
107+
isMountArgType :: RunMountArg -> Bool
108+
isMountArgType (MountArgType _) = True
109+
isMountArgType _ = False
112110

113111
bindMount :: [RunMountArg] -> Parser BindOpts
114112
bindMount args =
@@ -188,6 +186,7 @@ validArgs typeName allowed required args =
188186
[] -> result
189187
missing -> Left $ MissingArgument missing
190188
where
189+
checkValidArg :: RunMountArg -> (Either DockerfileError [RunMountArg], Set.Set Text) -> (Either DockerfileError [RunMountArg], Set.Set Text)
191190
checkValidArg _ x@(Left _, _) = x
192191
checkValidArg a (Right as, seen) =
193192
let name = toArgName a
@@ -196,38 +195,22 @@ validArgs typeName allowed required args =
196195
(_, True) -> (Left (DuplicateArgument name), seen)
197196
(True, False) -> (Right (a : as), Set.insert name seen)
198197

199-
mountChoices :: (?esc :: Char) => MountType -> Parser RunMountArg
200-
mountChoices mountType =
201-
choice $
202-
case mountType of
203-
Bind ->
204-
[ mountArgTarget,
205-
mountArgSource,
206-
mountArgFromImage,
207-
mountArgReadOnly
208-
]
209-
Cache ->
210-
[ mountArgTarget,
211-
mountArgSource,
212-
mountArgFromImage,
213-
mountArgReadOnly,
214-
mountArgId,
215-
mountArgSharing,
216-
mountArgMode,
217-
mountArgUid,
218-
mountArgGid
219-
]
220-
Tmpfs -> [mountArgTarget]
221-
_ -> -- Secret and Ssh
222-
[ mountArgTarget,
223-
mountArgId,
224-
mountArgRequired,
225-
mountArgSource,
226-
mountArgMode,
227-
mountArgUid,
228-
mountArgGid,
229-
mountArgEnv
230-
]
198+
mountArgs :: (?esc :: Char) => Parser RunMountArg
199+
mountArgs =
200+
choice
201+
[ mountArgEnv,
202+
mountArgFromImage,
203+
mountArgGid,
204+
mountArgId,
205+
mountArgMode,
206+
mountArgReadOnly,
207+
mountArgRequired,
208+
mountArgSharing,
209+
mountArgSource,
210+
mountArgTarget,
211+
mountArgType,
212+
mountArgUid
213+
]
231214

232215
stringArg :: (?esc :: Char) => Parser Text
233216
stringArg = choice [doubleQuotedString, someUnless "a string" (== ',')]
@@ -319,6 +302,19 @@ mountArgTarget = do
319302
label "target=" $ choice [string "target=", string "dst=", string "destination="]
320303
MountArgTarget . TargetPath <$> stringArg
321304

305+
mountArgType :: Parser RunMountArg
306+
mountArgType = MountArgType <$> key "type" mountType
307+
308+
mountType :: Parser MountType
309+
mountType =
310+
choice
311+
[ Bind <$ string "bind",
312+
Cache <$ string "cache",
313+
Tmpfs <$ string "tmpfs",
314+
Secret <$ string "secret",
315+
Ssh <$ string "ssh"
316+
]
317+
322318
mountArgUid :: (?esc :: Char) => Parser RunMountArg
323319
mountArgUid = MountArgUid <$> key "uid" stringArg
324320

test/Language/Docker/ParseRunSpec.hs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,41 @@ spec = do
7272
Run $ RunArgs (ArgumentsText "echo foo") flags2,
7373
Run $ RunArgs (ArgumentsText "echo foo") flags3
7474
]
75+
76+
it "--mount=type=cache with id from variable/arg" $
77+
let file =
78+
Text.unlines
79+
[ "ARG debian_version=12",
80+
"FROM debian:bookworm-slim",
81+
"RUN --mount=id=debian:${debian_version},type=cache,target=/foo foo bar"
82+
]
83+
flags = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo", cCacheId = Just "debian:${debian_version}"})}
84+
in assertAst
85+
file
86+
[ Arg "debian_version" (Just "12"),
87+
From $ BaseImage
88+
{ image = Image
89+
{ registryName = Nothing,
90+
imageName="debian"
91+
},
92+
tag = Just Tag { unTag="bookworm-slim" },
93+
digest = Nothing,
94+
alias = Nothing,
95+
platform = Nothing
96+
},
97+
Run $ RunArgs (ArgumentsText "foo bar") flags
98+
]
99+
100+
it "--mount=type=cache,dst=/foo" $
101+
let file = Text.unlines [ "RUN --mount=type=cache,dst=/foo echo foo" ]
102+
flags = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo"})}
103+
in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flags ]
104+
105+
it "--mount=dst=/foo,type=cache" $
106+
let file = Text.unlines [ "RUN --mount=dst=/foo,type=cache echo foo" ]
107+
flags = def {mount = Set.singleton $ CacheMount (def {cTarget = "/foo"})}
108+
in assertAst file [ Run $ RunArgs (ArgumentsText "echo foo") flags ]
109+
75110
it "--mount=type=cache with all modifiers" $
76111
let file =
77112
Text.unlines
@@ -99,6 +134,33 @@ spec = do
99134
file
100135
[ Run $ RunArgs (ArgumentsText "echo foo") flags
101136
]
137+
it "--mount=type=cache with all modifiers, different order" $
138+
let file =
139+
Text.unlines
140+
[ "RUN --mount=readonly,sharing=private,id=a,type=cache,from=ubuntu,source=/bar,destination=/foo,mode=0700,uid=0,gid=0 echo foo"
141+
]
142+
flags =
143+
def
144+
{ mount =
145+
Set.singleton $
146+
CacheMount
147+
( def
148+
{ cTarget = "/foo",
149+
cSharing = Just Private,
150+
cCacheId = Just "a",
151+
cReadOnly = Just True,
152+
cFromImage = Just "ubuntu",
153+
cSource = Just "/bar",
154+
cMode = Just "0700",
155+
cUid = Just "0",
156+
cGid = Just "0"
157+
}
158+
)
159+
}
160+
in assertAst
161+
file
162+
[ Run $ RunArgs (ArgumentsText "echo foo") flags
163+
]
102164
it "--mount=type=tmpfs" $
103165
let file = Text.unlines ["RUN --mount=type=tmpfs,target=/foo echo foo"]
104166
flags = def {mount = Set.singleton $ TmpfsMount (def {tTarget = "/foo"})}
@@ -639,3 +701,8 @@ spec = do
639701
file
640702
[ Run $ RunArgs ( ArgumentsText "ls && cat" ) flags
641703
]
704+
705+
describe "Parse RUN instructions - invalid cases" $ do
706+
it "fail because of multiple types in --mount" $
707+
let line = "RUN --mount=type=cache,type=tmpfs,target=/foo foo bar"
708+
in expectFail line

0 commit comments

Comments
 (0)